OSDN Git Service

発言のダブルクリック時の動作を列挙体ListItemDoubleClickActionTypeで定義
[opentween/open-tween.git] / OpenTween / Tween.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 //           (c) 2008-2011 Moz (@syo68k)
4 //           (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 //           (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 //           (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 //           (c) 2011      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
9 //
10 // This file is part of OpenTween.
11 //
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
15 // any later version.
16 //
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
20 // for more details.
21 //
22 // You should have received a copy of the GNU General public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
26
27 #nullable enable
28
29 // コンパイル後コマンド
30 // "c:\Program Files\Microsoft.NET\SDK\v2.0\Bin\sgen.exe" /f /a:"$(TargetPath)"
31 // "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\sgen.exe" /f /a:"$(TargetPath)"
32
33 using System;
34 using System.Collections.Concurrent;
35 using System.Collections.Generic;
36 using System.ComponentModel;
37 using System.Diagnostics;
38 using System.Diagnostics.CodeAnalysis;
39 using System.Drawing;
40 using System.Globalization;
41 using System.IO;
42 using System.Linq;
43 using System.Media;
44 using System.Net;
45 using System.Net.Http;
46 using System.Reflection;
47 using System.Runtime.InteropServices;
48 using System.Text;
49 using System.Text.RegularExpressions;
50 using System.Threading;
51 using System.Threading.Tasks;
52 using System.Windows.Forms;
53 using OpenTween.Api;
54 using OpenTween.Api.DataModel;
55 using OpenTween.Connection;
56 using OpenTween.MediaUploadServices;
57 using OpenTween.Models;
58 using OpenTween.OpenTweenCustomControl;
59 using OpenTween.Setting;
60 using OpenTween.Thumbnail;
61
62 namespace OpenTween
63 {
64     public partial class TweenMain : OTBaseForm
65     {
66         // 各種設定
67
68         /// <summary>画面サイズ</summary>
69         private Size mySize;
70
71         /// <summary>画面位置</summary>
72         private Point myLoc;
73
74         /// <summary>区切り位置</summary>
75         private int mySpDis;
76
77         /// <summary>発言欄区切り位置</summary>
78         private int mySpDis2;
79
80         /// <summary>プレビュー区切り位置</summary>
81         private int mySpDis3;
82
83         /// <summary>アイコンサイズ</summary>
84         /// <remarks>
85         /// 現在は16、24、48の3種類。将来直接数字指定可能とする
86         /// 注:24x24の場合に26と指定しているのはMSゴシック系フォントのための仕様
87         /// </remarks>
88         private int iconSz;
89
90         private bool iconCol; // 1列表示の時true(48サイズのとき)
91
92         // 雑多なフラグ類
93         private bool initial; // true:起動時処理中
94         private bool initialLayout = true;
95         private bool ignoreConfigSave; // true:起動時処理中
96
97         /// <summary>タブドラッグ中フラグ(DoDragDropを実行するかの判定用)</summary>
98         private bool tabDrag;
99
100         private TabPage? beforeSelectedTab; // タブが削除されたときに前回選択されていたときのタブを選択する為に保持
101         private Point tabMouseDownPoint;
102
103         /// <summary>右クリックしたタブの名前(Tabコントロール機能不足対応)</summary>
104         private string? rclickTabName;
105
106         private readonly object syncObject = new object(); // ロック用
107
108         private const string DetailHtmlFormatHead =
109             "<head><meta http-equiv=\"X-UA-Compatible\" content=\"IE=8\">"
110             + "<style type=\"text/css\"><!-- "
111             + "body, p, pre {margin: 0;} "
112             + "body {font-family: \"%FONT_FAMILY%\", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} "
113             + "pre {font-family: inherit;} "
114             + "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
115             + "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
116             + ".quote-tweet {border: 1px solid #ccc; margin: 1em; padding: 0.5em;} "
117             + ".quote-tweet.reply {border-color: rgb(%BG_REPLY_COLOR%);} "
118             + ".quote-tweet-link {color: inherit !important; text-decoration: none;}"
119             + "--></style>"
120             + "</head>";
121
122         private const string DetailHtmlFormatTemplateMono =
123             "<html>" + DetailHtmlFormatHead + "<body><pre>%CONTENT_HTML%</pre></body></html>";
124
125         private const string DetailHtmlFormatTemplateNormal =
126             "<html>" + DetailHtmlFormatHead + "<body><p>%CONTENT_HTML%</p></body></html>";
127
128         private string detailHtmlFormatPreparedTemplate = null!;
129
130         private bool myStatusError = false;
131         private bool myStatusOnline = false;
132         private bool soundfileListup = false;
133         private FormWindowState formWindowState = FormWindowState.Normal; // フォームの状態保存用 通知領域からアイコンをクリックして復帰した際に使用する
134
135         // twitter解析部
136         private readonly TwitterApi twitterApi = new TwitterApi(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret);
137         private Twitter tw = null!;
138
139         // Growl呼び出し部
140         private readonly GrowlHelper gh = new GrowlHelper(ApplicationSettings.ApplicationName);
141
142         // サブ画面インスタンス
143
144         /// <summary>検索画面インスタンス</summary>
145         internal SearchWordDialog SearchDialog = new SearchWordDialog();
146
147         private readonly OpenURL urlDialog = new OpenURL();
148
149         /// <summary>@id補助</summary>
150         public AtIdSupplement AtIdSupl = null!;
151
152         /// <summary>Hashtag補助</summary>
153         public AtIdSupplement HashSupl = null!;
154
155         public HashtagManage HashMgr = null!;
156
157         // 表示フォント、色、アイコン
158
159         /// <summary>未読用フォント</summary>
160         private Font fntUnread = null!;
161
162         /// <summary>未読用文字色</summary>
163         private Color clUnread;
164
165         /// <summary>既読用フォント</summary>
166         private Font fntReaded = null!;
167
168         /// <summary>既読用文字色</summary>
169         private Color clReaded;
170
171         /// <summary>Fav用文字色</summary>
172         private Color clFav;
173
174         /// <summary>片思い用文字色</summary>
175         private Color clOWL;
176
177         /// <summary>Retweet用文字色</summary>
178         private Color clRetweet;
179
180         /// <summary>選択中の行用文字色</summary>
181         private readonly Color clHighLight = Color.FromKnownColor(KnownColor.HighlightText);
182
183         /// <summary>発言詳細部用フォント</summary>
184         private Font fntDetail = null!;
185
186         /// <summary>発言詳細部用色</summary>
187         private Color clDetail;
188
189         /// <summary>発言詳細部用リンク文字色</summary>
190         private Color clDetailLink;
191
192         /// <summary>発言詳細部用背景色</summary>
193         private Color clDetailBackcolor;
194
195         /// <summary>自分の発言用背景色</summary>
196         private Color clSelf;
197
198         /// <summary>自分宛返信用背景色</summary>
199         private Color clAtSelf;
200
201         /// <summary>選択発言者の他の発言用背景色</summary>
202         private Color clTarget;
203
204         /// <summary>選択発言中の返信先用背景色</summary>
205         private Color clAtTarget;
206
207         /// <summary>選択発言者への返信発言用背景色</summary>
208         private Color clAtFromTarget;
209
210         /// <summary>選択発言の唯一@先</summary>
211         private Color clAtTo;
212
213         /// <summary>リスト部通常発言背景色</summary>
214         private Color clListBackcolor;
215
216         /// <summary>入力欄背景色</summary>
217         private Color clInputBackcolor;
218
219         /// <summary>入力欄文字色</summary>
220         private Color clInputFont;
221
222         /// <summary>入力欄フォント</summary>
223         private Font fntInputFont = null!;
224
225         /// <summary>アイコン画像リスト</summary>
226         private ImageCache iconCache = null!;
227
228         /// <summary>タスクトレイアイコン:通常時 (At.ico)</summary>
229         private Icon nIconAt = null!;
230
231         /// <summary>タスクトレイアイコン:通信エラー時 (AtRed.ico)</summary>
232         private Icon nIconAtRed = null!;
233
234         /// <summary>タスクトレイアイコン:オフライン時 (AtSmoke.ico)</summary>
235         private Icon nIconAtSmoke = null!;
236
237         /// <summary>タスクトレイアイコン:更新中 (Refresh.ico)</summary>
238         private Icon[] nIconRefresh = new Icon[4];
239
240         /// <summary>未読のあるタブ用アイコン (Tab.ico)</summary>
241         private Icon tabIcon = null!;
242
243         /// <summary>画面左上のアイコン (Main.ico)</summary>
244         private Icon mainIcon = null!;
245
246         private Icon replyIcon = null!;
247         private Icon replyIconBlink = null!;
248
249         private readonly ImageList listViewImageList = new ImageList(); // ListViewItemの高さ変更用
250
251         private PostClass? anchorPost;
252         private bool anchorFlag; // true:関連発言移動中(関連移動以外のオペレーションをするとfalseへ。trueだとリスト背景色をアンカー発言選択中として描画)
253
254         /// <summary>発言履歴</summary>
255         private readonly List<StatusTextHistory> history = new List<StatusTextHistory>();
256
257         /// <summary>発言履歴カレントインデックス</summary>
258         private int hisIdx;
259
260         // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない)
261
262         /// <summary>リプライ先のステータスID・スクリーン名</summary>
263         private (long StatusId, string ScreenName)? inReplyTo = null;
264
265         // 時速表示用
266         private readonly List<DateTimeUtc> postTimestamps = new List<DateTimeUtc>();
267         private readonly List<DateTimeUtc> favTimestamps = new List<DateTimeUtc>();
268
269         // 以下DrawItem関連
270         private readonly SolidBrush brsHighLight = new SolidBrush(Color.FromKnownColor(KnownColor.Highlight));
271         private SolidBrush brsBackColorMine = null!;
272         private SolidBrush brsBackColorAt = null!;
273         private SolidBrush brsBackColorYou = null!;
274         private SolidBrush brsBackColorAtYou = null!;
275         private SolidBrush brsBackColorAtFromTarget = null!;
276         private SolidBrush brsBackColorAtTo = null!;
277         private SolidBrush brsBackColorNone = null!;
278
279         /// <summary>Listにフォーカスないときの選択行の背景色</summary>
280         private readonly SolidBrush brsDeactiveSelection = new SolidBrush(Color.FromKnownColor(KnownColor.ButtonFace));
281
282         private readonly StringFormat sfTab = new StringFormat();
283
284         //////////////////////////////////////////////////////////////////////////////////////////////////////////
285         private TabInformations statuses = null!;
286
287         /// <summary>
288         /// 現在表示している発言一覧の <see cref="ListView"/> に対するキャッシュ
289         /// </summary>
290         /// <remarks>
291         /// キャッシュクリアのために null が代入されることがあるため、
292         /// 使用する場合には <see cref="listItemCache"/> に対して直接メソッド等を呼び出さずに
293         /// 一旦ローカル変数に代入してから参照すること。
294         /// </remarks>
295         private ListViewItemCache? listItemCache = null;
296
297         internal class ListViewItemCache
298         {
299             /// <summary>アイテムをキャッシュする対象の <see cref="ListView"/></summary>
300             public ListView TargetList { get; set; } = null!;
301
302             /// <summary>キャッシュする範囲の開始インデックス</summary>
303             public int StartIndex { get; set; }
304
305             /// <summary>キャッシュする範囲の終了インデックス</summary>
306             public int EndIndex { get; set; }
307
308             /// <summary>キャッシュされた範囲に対応する <see cref="ListViewItem"/> と <see cref="PostClass"/> の組</summary>
309             public (ListViewItem, PostClass)[] Cache { get; set; } = null!;
310
311             /// <summary>キャッシュされたアイテムの件数</summary>
312             public int Count
313                 => this.EndIndex - this.StartIndex + 1;
314
315             /// <summary>指定されたインデックスがキャッシュの範囲内であるか判定します</summary>
316             /// <returns><paramref name="index"/> がキャッシュの範囲内であれば true、それ以外は false</returns>
317             public bool Contains(int index)
318                 => index >= this.StartIndex && index <= this.EndIndex;
319
320             /// <summary>指定されたインデックスの範囲が全てキャッシュの範囲内であるか判定します</summary>
321             /// <returns><paramref name="rangeStart"/> から <paramref name="rangeEnd"/> の範囲が全てキャッシュの範囲内であれば true、それ以外は false</returns>
322             public bool IsSupersetOf(int rangeStart, int rangeEnd)
323                 => rangeStart >= this.StartIndex && rangeEnd <= this.EndIndex;
324
325             /// <summary>指定されたインデックスの <see cref="ListViewItem"/> と <see cref="PostClass"/> をキャッシュから取得することを試みます</summary>
326             /// <returns>取得に成功すれば true、それ以外は false</returns>
327             public bool TryGetValue(int index, [NotNullWhen(true)] out ListViewItem? item, [NotNullWhen(true)] out PostClass? post)
328             {
329                 if (this.Contains(index))
330                 {
331                     (item, post) = this.Cache[index - this.StartIndex];
332                     return true;
333                 }
334                 else
335                 {
336                     item = null;
337                     post = null;
338                     return false;
339                 }
340             }
341         }
342
343         private bool isColumnChanged = false;
344
345         private const int MaxWorderThreads = 20;
346         private readonly SemaphoreSlim workerSemaphore = new SemaphoreSlim(MaxWorderThreads);
347         private readonly CancellationTokenSource workerCts = new CancellationTokenSource();
348         private readonly IProgress<string> workerProgress = null!;
349
350         private int unreadCounter = -1;
351         private int unreadAtCounter = -1;
352
353         private readonly string[] columnOrgText = new string[9];
354         private readonly string[] columnText = new string[9];
355
356         private bool doFavRetweetFlags = false;
357
358         //////////////////////////////////////////////////////////////////////////////////////////////////////////
359
360         private readonly TimelineScheduler timelineScheduler = new TimelineScheduler();
361         private DebounceTimer selectionDebouncer = null!;
362         private DebounceTimer saveConfigDebouncer = null!;
363
364         private string recommendedStatusFooter = null!;
365         private bool urlMultibyteSplit = false;
366         private bool preventSmsCommand = true;
367
368         // URL短縮のUndo用
369         private struct UrlUndo
370         {
371             public string Before;
372             public string After;
373         }
374
375         private List<UrlUndo>? urlUndoBuffer = null;
376
377         private readonly struct ReplyChain
378         {
379             public readonly long OriginalId;
380             public readonly long InReplyToId;
381             public readonly TabModel OriginalTab;
382
383             public ReplyChain(long originalId, long inReplyToId, TabModel originalTab)
384             {
385                 this.OriginalId = originalId;
386                 this.InReplyToId = inReplyToId;
387                 this.OriginalTab = originalTab;
388             }
389         }
390
391         /// <summary>[, ]でのリプライ移動の履歴</summary>
392         private Stack<ReplyChain>? replyChains;
393
394         /// <summary>ポスト選択履歴</summary>
395         private readonly Stack<(TabModel, PostClass?)> selectPostChains = new Stack<(TabModel, PostClass?)>();
396
397         public TabModel CurrentTab
398             => this.statuses.SelectedTab;
399
400         public string CurrentTabName
401             => this.statuses.SelectedTabName;
402
403         public TabPage CurrentTabPage
404             => this.ListTab.TabPages[this.statuses.Tabs.IndexOf(this.CurrentTabName)];
405
406         public DetailsListView CurrentListView
407             => (DetailsListView)this.CurrentTabPage.Tag;
408
409         public PostClass? CurrentPost
410             => this.CurrentTab.SelectedPost;
411
412         /// <summary>検索処理タイプ</summary>
413         internal enum SEARCHTYPE
414         {
415             DialogSearch,
416             NextSearch,
417             PrevSearch,
418         }
419
420         private class StatusTextHistory
421         {
422             public string Status { get; } = "";
423
424             public (long StatusId, string ScreenName)? InReplyTo { get; } = null;
425
426             /// <summary>画像投稿サービス名</summary>
427             public string ImageService { get; set; } = "";
428
429             public IMediaItem[]? MediaItems { get; set; } = null;
430
431             public StatusTextHistory()
432             {
433             }
434
435             public StatusTextHistory(string status, (long StatusId, string ScreenName)? inReplyTo)
436             {
437                 this.Status = status;
438                 this.InReplyTo = inReplyTo;
439             }
440         }
441
442         private void TweenMain_Activated(object sender, EventArgs e)
443         {
444             // 画面がアクティブになったら、発言欄の背景色戻す
445             if (this.StatusText.Focused)
446             {
447                 this.StatusText_Enter(this.StatusText, System.EventArgs.Empty);
448             }
449         }
450
451         private bool disposed = false;
452
453         /// <summary>
454         /// 使用中のリソースをすべてクリーンアップします。
455         /// </summary>
456         /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
457         protected override void Dispose(bool disposing)
458         {
459             base.Dispose(disposing);
460
461             if (this.disposed)
462                 return;
463
464             if (disposing)
465             {
466                 this.components?.Dispose();
467
468                 // 後始末
469                 this.SearchDialog.Dispose();
470                 this.urlDialog.Dispose();
471                 this.nIconAt?.Dispose();
472                 this.nIconAtRed?.Dispose();
473                 this.nIconAtSmoke?.Dispose();
474                 foreach (var iconRefresh in this.nIconRefresh)
475                 {
476                     iconRefresh?.Dispose();
477                 }
478                 this.tabIcon?.Dispose();
479                 this.mainIcon?.Dispose();
480                 this.replyIcon?.Dispose();
481                 this.replyIconBlink?.Dispose();
482                 this.listViewImageList.Dispose();
483                 this.brsHighLight.Dispose();
484                 this.brsBackColorMine?.Dispose();
485                 this.brsBackColorAt?.Dispose();
486                 this.brsBackColorYou?.Dispose();
487                 this.brsBackColorAtYou?.Dispose();
488                 this.brsBackColorAtFromTarget?.Dispose();
489                 this.brsBackColorAtTo?.Dispose();
490                 this.brsBackColorNone?.Dispose();
491                 this.brsDeactiveSelection?.Dispose();
492                 this.sfTab.Dispose();
493
494                 this.timelineScheduler.Dispose();
495                 this.workerCts.Cancel();
496
497                 if (this.iconCache != null)
498                 {
499                     this.iconCache.CancelAsync();
500                     this.iconCache.Dispose();
501                 }
502
503                 this.thumbnailTokenSource?.Dispose();
504
505                 this.twitterApi.Dispose();
506                 this.hookGlobalHotkey.Dispose();
507             }
508
509             // 終了時にRemoveHandlerしておかないとメモリリークする
510             // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx
511             Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged;
512             Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged;
513
514             this.disposed = true;
515         }
516
517         private void LoadIcons()
518         {
519             // Icons フォルダ以下のアイコンを読み込み(着せ替えアイコン対応)
520             var iconsDir = Path.Combine(Application.StartupPath, "Icons");
521
522             // ウィンドウ左上のアイコン
523             var iconMain = this.LoadIcon(Path.Combine(iconsDir, "MIcon.ico"));
524
525             // タブ見出し未読表示アイコン
526             var iconTab = this.LoadIcon(Path.Combine(iconsDir, "Tab.ico"));
527
528             // タスクトレイ: 通常時アイコン
529             var iconAt = this.LoadIcon(Path.Combine(iconsDir, "At.ico"));
530
531             // タスクトレイ: エラー時アイコン
532             var iconAtRed = this.LoadIcon(Path.Combine(iconsDir, "AtRed.ico"));
533
534             // タスクトレイ: オフライン時アイコン
535             var iconAtSmoke = this.LoadIcon(Path.Combine(iconsDir, "AtSmoke.ico"));
536
537             // タスクトレイ: Reply通知アイコン (最大2枚でアニメーション可能)
538             var iconReply = this.LoadIcon(Path.Combine(iconsDir, "Reply.ico"));
539             var iconReplyBlink = this.LoadIcon(Path.Combine(iconsDir, "ReplyBlink.ico"));
540
541             // タスクトレイ: 更新中アイコン (最大4枚でアニメーション可能)
542             var iconRefresh1 = this.LoadIcon(Path.Combine(iconsDir, "Refresh.ico"));
543             var iconRefresh2 = this.LoadIcon(Path.Combine(iconsDir, "Refresh2.ico"));
544             var iconRefresh3 = this.LoadIcon(Path.Combine(iconsDir, "Refresh3.ico"));
545             var iconRefresh4 = this.LoadIcon(Path.Combine(iconsDir, "Refresh4.ico"));
546
547             // 読み込んだアイコンを設定 (不足するアイコンはデフォルトのものを設定)
548
549             this.mainIcon = iconMain ?? Properties.Resources.MIcon;
550             this.tabIcon = iconTab ?? Properties.Resources.TabIcon;
551             this.nIconAt = iconAt ?? iconMain ?? Properties.Resources.At;
552             this.nIconAtRed = iconAtRed ?? Properties.Resources.AtRed;
553             this.nIconAtSmoke = iconAtSmoke ?? Properties.Resources.AtSmoke;
554
555             if (iconReply != null && iconReplyBlink != null)
556             {
557                 this.replyIcon = iconReply;
558                 this.replyIconBlink = iconReplyBlink;
559             }
560             else
561             {
562                 this.replyIcon = iconReply ?? iconReplyBlink ?? Properties.Resources.Reply;
563                 this.replyIconBlink = this.nIconAt;
564             }
565
566             if (iconRefresh1 == null)
567             {
568                 this.nIconRefresh = new[]
569                 {
570                     Properties.Resources.Refresh, Properties.Resources.Refresh2,
571                     Properties.Resources.Refresh3, Properties.Resources.Refresh4,
572                 };
573             }
574             else if (iconRefresh2 == null)
575             {
576                 this.nIconRefresh = new[] { iconRefresh1 };
577             }
578             else if (iconRefresh3 == null)
579             {
580                 this.nIconRefresh = new[] { iconRefresh1, iconRefresh2 };
581             }
582             else if (iconRefresh4 == null)
583             {
584                 this.nIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3 };
585             }
586             else // iconRefresh1 から iconRefresh4 まで全て揃っている
587             {
588                 this.nIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3, iconRefresh4 };
589             }
590         }
591
592         private Icon? LoadIcon(string filePath)
593         {
594             if (!File.Exists(filePath))
595                 return null;
596
597             try
598             {
599                 return new Icon(filePath);
600             }
601             catch (Exception)
602             {
603                 return null;
604             }
605         }
606
607         private void InitColumns(ListView list, bool startup)
608         {
609             this.InitColumnText();
610
611             ColumnHeader[]? columns = null;
612             try
613             {
614                 if (this.iconCol)
615                 {
616                     columns = new[]
617                     {
618                         new ColumnHeader(), // アイコン
619                         new ColumnHeader(), // 本文
620                     };
621
622                     columns[0].Text = this.columnText[0];
623                     columns[1].Text = this.columnText[2];
624
625                     if (startup)
626                     {
627                         var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / SettingManager.Local.ScaleDimension.Width;
628
629                         columns[0].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width1);
630                         columns[1].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width3);
631                         columns[0].DisplayIndex = 0;
632                         columns[1].DisplayIndex = 1;
633                     }
634                     else
635                     {
636                         var idx = 0;
637                         foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
638                         {
639                             columns[idx].Width = curListColumn.Width;
640                             columns[idx].DisplayIndex = curListColumn.DisplayIndex;
641                             idx++;
642                         }
643                     }
644                 }
645                 else
646                 {
647                     columns = new[]
648                     {
649                         new ColumnHeader(), // アイコン
650                         new ColumnHeader(), // ニックネーム
651                         new ColumnHeader(), // 本文
652                         new ColumnHeader(), // 日付
653                         new ColumnHeader(), // ユーザID
654                         new ColumnHeader(), // 未読
655                         new ColumnHeader(), // マーク&プロテクト
656                         new ColumnHeader(), // ソース
657                     };
658
659                     foreach (var i in Enumerable.Range(0, columns.Length))
660                         columns[i].Text = this.columnText[i];
661
662                     if (startup)
663                     {
664                         var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / SettingManager.Local.ScaleDimension.Width;
665
666                         columns[0].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width1);
667                         columns[1].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width2);
668                         columns[2].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width3);
669                         columns[3].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width4);
670                         columns[4].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width5);
671                         columns[5].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width6);
672                         columns[6].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width7);
673                         columns[7].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width8);
674
675                         var displayIndex = new[]
676                         {
677                             SettingManager.Local.DisplayIndex1, SettingManager.Local.DisplayIndex2,
678                             SettingManager.Local.DisplayIndex3, SettingManager.Local.DisplayIndex4,
679                             SettingManager.Local.DisplayIndex5, SettingManager.Local.DisplayIndex6,
680                             SettingManager.Local.DisplayIndex7, SettingManager.Local.DisplayIndex8,
681                         };
682
683                         foreach (var i in Enumerable.Range(0, displayIndex.Length))
684                         {
685                             columns[i].DisplayIndex = displayIndex[i];
686                         }
687                     }
688                     else
689                     {
690                         var idx = 0;
691                         foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
692                         {
693                             columns[idx].Width = curListColumn.Width;
694                             columns[idx].DisplayIndex = curListColumn.DisplayIndex;
695                             idx++;
696                         }
697                     }
698                 }
699
700                 list.Columns.AddRange(columns);
701
702                 columns = null;
703             }
704             finally
705             {
706                 if (columns != null)
707                 {
708                     foreach (var column in columns)
709                         column?.Dispose();
710                 }
711             }
712         }
713
714         private void InitColumnText()
715         {
716             this.columnText[0] = "";
717             this.columnText[1] = Properties.Resources.AddNewTabText2;
718             this.columnText[2] = Properties.Resources.AddNewTabText3;
719             this.columnText[3] = Properties.Resources.AddNewTabText4_2;
720             this.columnText[4] = Properties.Resources.AddNewTabText5;
721             this.columnText[5] = "";
722             this.columnText[6] = "";
723             this.columnText[7] = "Source";
724
725             this.columnOrgText[0] = "";
726             this.columnOrgText[1] = Properties.Resources.AddNewTabText2;
727             this.columnOrgText[2] = Properties.Resources.AddNewTabText3;
728             this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2;
729             this.columnOrgText[4] = Properties.Resources.AddNewTabText5;
730             this.columnOrgText[5] = "";
731             this.columnOrgText[6] = "";
732             this.columnOrgText[7] = "Source";
733
734             var c = this.statuses.SortMode switch
735             {
736                 ComparerMode.Nickname => 1, // ニックネーム
737                 ComparerMode.Data => 2, // 本文
738                 ComparerMode.Id => 3, // 時刻=発言Id
739                 ComparerMode.Name => 4, // 名前
740                 ComparerMode.Source => 7, // Source
741                 _ => 0,
742             };
743
744             if (this.iconCol)
745             {
746                 if (this.statuses.SortOrder == SortOrder.Descending)
747                 {
748                     // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
749                     this.columnText[2] = this.columnOrgText[2] + "▾";
750                 }
751                 else
752                 {
753                     // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
754                     this.columnText[2] = this.columnOrgText[2] + "▴";
755                 }
756             }
757             else
758             {
759                 if (this.statuses.SortOrder == SortOrder.Descending)
760                 {
761                     // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
762                     this.columnText[c] = this.columnOrgText[c] + "▾";
763                 }
764                 else
765                 {
766                     // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
767                     this.columnText[c] = this.columnOrgText[c] + "▴";
768                 }
769             }
770         }
771
772         private void InitializeTraceFrag()
773         {
774 #if DEBUG
775             this.TraceOutToolStripMenuItem.Checked = true;
776             MyCommon.TraceFlag = true;
777 #endif
778             if (!MyCommon.FileVersion.EndsWith("0", StringComparison.Ordinal))
779             {
780                 this.TraceOutToolStripMenuItem.Checked = true;
781                 MyCommon.TraceFlag = true;
782             }
783         }
784
785         private void TweenMain_Load(object sender, EventArgs e)
786         {
787             this.ignoreConfigSave = true;
788             this.Visible = false;
789
790             if (ApplicationEvents.StartupOptions.ContainsKey("d"))
791                 MyCommon.TraceFlag = true;
792
793             this.InitializeTraceFrag();
794
795             Microsoft.Win32.SystemEvents.PowerModeChanged += this.SystemEvents_PowerModeChanged;
796
797             Regex.CacheSize = 100;
798
799             // 発言保持クラス
800             this.statuses = TabInformations.GetInstance();
801
802             // アイコン設定
803             this.LoadIcons();
804             this.Icon = this.mainIcon;              // メインフォーム(TweenMain)
805             this.NotifyIcon1.Icon = this.nIconAt;      // タスクトレイ
806             this.TabImage.Images.Add(this.tabIcon);    // タブ見出し
807
808             // <<<<<<<<<設定関連>>>>>>>>>
809             // 設定読み出し
810             this.LoadConfig();
811
812             // 現在の DPI と設定保存時の DPI との比を取得する
813             var configScaleFactor = SettingManager.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
814
815             // UIフォント設定
816             var fontUIGlobal = SettingManager.Local.FontUIGlobal;
817             if (fontUIGlobal != null)
818             {
819                 OTBaseForm.GlobalFont = fontUIGlobal;
820                 this.Font = fontUIGlobal;
821             }
822
823             // 不正値チェック
824             if (!ApplicationEvents.StartupOptions.ContainsKey("nolimit"))
825             {
826                 if (SettingManager.Common.TimelinePeriod < 15 && SettingManager.Common.TimelinePeriod > 0)
827                     SettingManager.Common.TimelinePeriod = 15;
828
829                 if (SettingManager.Common.ReplyPeriod < 15 && SettingManager.Common.ReplyPeriod > 0)
830                     SettingManager.Common.ReplyPeriod = 15;
831
832                 if (SettingManager.Common.DMPeriod < 15 && SettingManager.Common.DMPeriod > 0)
833                     SettingManager.Common.DMPeriod = 15;
834
835                 if (SettingManager.Common.PubSearchPeriod < 30 && SettingManager.Common.PubSearchPeriod > 0)
836                     SettingManager.Common.PubSearchPeriod = 30;
837
838                 if (SettingManager.Common.UserTimelinePeriod < 15 && SettingManager.Common.UserTimelinePeriod > 0)
839                     SettingManager.Common.UserTimelinePeriod = 15;
840
841                 if (SettingManager.Common.ListsPeriod < 15 && SettingManager.Common.ListsPeriod > 0)
842                     SettingManager.Common.ListsPeriod = 15;
843             }
844
845             if (!Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Timeline, SettingManager.Common.CountApi))
846                 SettingManager.Common.CountApi = 60;
847             if (!Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Reply, SettingManager.Common.CountApiReply))
848                 SettingManager.Common.CountApiReply = 40;
849
850             if (SettingManager.Common.MoreCountApi != 0 && !Twitter.VerifyMoreApiResultCount(SettingManager.Common.MoreCountApi))
851                 SettingManager.Common.MoreCountApi = 200;
852             if (SettingManager.Common.FirstCountApi != 0 && !Twitter.VerifyFirstApiResultCount(SettingManager.Common.FirstCountApi))
853                 SettingManager.Common.FirstCountApi = 100;
854
855             if (SettingManager.Common.FavoritesCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Favorites, SettingManager.Common.FavoritesCountApi))
856                 SettingManager.Common.FavoritesCountApi = 40;
857             if (SettingManager.Common.ListCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.List, SettingManager.Common.ListCountApi))
858                 SettingManager.Common.ListCountApi = 100;
859             if (SettingManager.Common.SearchCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.PublicSearch, SettingManager.Common.SearchCountApi))
860                 SettingManager.Common.SearchCountApi = 100;
861             if (SettingManager.Common.UserTimelineCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.UserTimeline, SettingManager.Common.UserTimelineCountApi))
862                 SettingManager.Common.UserTimelineCountApi = 20;
863
864             // 廃止サービスが選択されていた場合ux.nuへ読み替え
865             if (SettingManager.Common.AutoShortUrlFirst < 0)
866                 SettingManager.Common.AutoShortUrlFirst = MyCommon.UrlConverter.Uxnu;
867
868             TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost;
869             this.tw = new Twitter(this.twitterApi);
870
871             // 認証関連
872             if (MyCommon.IsNullOrEmpty(SettingManager.Common.Token)) SettingManager.Common.UserName = "";
873             this.tw.Initialize(SettingManager.Common.Token, SettingManager.Common.TokenSecret, SettingManager.Common.UserName, SettingManager.Common.UserId);
874
875             this.initial = true;
876
877             Networking.Initialize();
878
879             var saveRequired = false;
880             var firstRun = false;
881
882             // ユーザー名、パスワードが未設定なら設定画面を表示(初回起動時など)
883             if (MyCommon.IsNullOrEmpty(this.tw.Username))
884             {
885                 saveRequired = true;
886                 firstRun = true;
887
888                 // 設定せずにキャンセルされたか、設定されたが依然ユーザー名が未設定ならプログラム終了
889                 if (this.ShowSettingDialog(showTaskbarIcon: true) != DialogResult.OK ||
890                     MyCommon.IsNullOrEmpty(this.tw.Username))
891                 {
892                     Application.Exit();  // 強制終了
893                     return;
894                 }
895             }
896
897             // Twitter用通信クラス初期化
898             Networking.DefaultTimeout = TimeSpan.FromSeconds(SettingManager.Common.DefaultTimeOut);
899             Networking.UploadImageTimeout = TimeSpan.FromSeconds(SettingManager.Common.UploadImageTimeout);
900             Networking.SetWebProxy(
901                 SettingManager.Local.ProxyType,
902                 SettingManager.Local.ProxyAddress,
903                 SettingManager.Local.ProxyPort,
904                 SettingManager.Local.ProxyUser,
905                 SettingManager.Local.ProxyPassword);
906             Networking.ForceIPv4 = SettingManager.Common.ForceIPv4;
907
908             TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost;
909             this.tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck;
910             this.tw.ReadOwnPost = SettingManager.Common.ReadOwnPost;
911             ShortUrl.Instance.DisableExpanding = !SettingManager.Common.TinyUrlResolve;
912             ShortUrl.Instance.BitlyAccessToken = SettingManager.Common.BitlyAccessToken;
913             ShortUrl.Instance.BitlyId = SettingManager.Common.BilyUser;
914             ShortUrl.Instance.BitlyKey = SettingManager.Common.BitlyPwd;
915
916             // アクセストークンが有効であるか確認する
917             // ここが Twitter API への最初のアクセスになるようにすること
918             try
919             {
920                 this.tw.VerifyCredentials();
921             }
922             catch (WebApiException ex)
923             {
924                 MessageBox.Show(
925                     this,
926                     string.Format(Properties.Resources.StartupAuthError_Text, ex.Message),
927                     ApplicationSettings.ApplicationName,
928                     MessageBoxButtons.OK,
929                     MessageBoxIcon.Warning);
930             }
931
932             // サムネイル関連の初期化
933             // プロキシ設定等の通信まわりの初期化が済んでから処理する
934             ThumbnailGenerator.InitializeGenerator();
935
936             var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
937             imgazyobizinet.Enabled = SettingManager.Common.EnableImgAzyobuziNet;
938             imgazyobizinet.DisabledInDM = SettingManager.Common.ImgAzyobuziNetDisabledInDM;
939
940             Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.twitterApi.Connection;
941
942             // 画像投稿サービス
943             this.ImageSelector.Initialize(this.tw, this.tw.Configuration, SettingManager.Common.UseImageServiceName, SettingManager.Common.UseImageService);
944
945             // ハッシュタグ/@id関連
946             this.AtIdSupl = new AtIdSupplement(SettingManager.AtIdList.AtIdList, "@");
947             this.HashSupl = new AtIdSupplement(SettingManager.Common.HashTags, "#");
948             this.HashMgr = new HashtagManage(this.HashSupl,
949                                     SettingManager.Common.HashTags.ToArray(),
950                                     SettingManager.Common.HashSelected,
951                                     SettingManager.Common.HashIsPermanent,
952                                     SettingManager.Common.HashIsHead,
953                                     SettingManager.Common.HashIsNotAddToAtReply);
954             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && this.HashMgr.IsPermanent) this.HashStripSplitButton.Text = this.HashMgr.UseHash;
955
956             // アイコンリスト作成
957             this.iconCache = new ImageCache();
958             this.tweetDetailsView.IconCache = this.iconCache;
959
960             // フォント&文字色&背景色保持
961             this.fntUnread = SettingManager.Local.FontUnread;
962             this.clUnread = SettingManager.Local.ColorUnread;
963             this.fntReaded = SettingManager.Local.FontRead;
964             this.clReaded = SettingManager.Local.ColorRead;
965             this.clFav = SettingManager.Local.ColorFav;
966             this.clOWL = SettingManager.Local.ColorOWL;
967             this.clRetweet = SettingManager.Local.ColorRetweet;
968             this.fntDetail = SettingManager.Local.FontDetail;
969             this.clDetail = SettingManager.Local.ColorDetail;
970             this.clDetailLink = SettingManager.Local.ColorDetailLink;
971             this.clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor;
972             this.clSelf = SettingManager.Local.ColorSelf;
973             this.clAtSelf = SettingManager.Local.ColorAtSelf;
974             this.clTarget = SettingManager.Local.ColorTarget;
975             this.clAtTarget = SettingManager.Local.ColorAtTarget;
976             this.clAtFromTarget = SettingManager.Local.ColorAtFromTarget;
977             this.clAtTo = SettingManager.Local.ColorAtTo;
978             this.clListBackcolor = SettingManager.Local.ColorListBackcolor;
979             this.clInputBackcolor = SettingManager.Local.ColorInputBackcolor;
980             this.clInputFont = SettingManager.Local.ColorInputFont;
981             this.fntInputFont = SettingManager.Local.FontInputFont;
982
983             this.brsBackColorMine = new SolidBrush(this.clSelf);
984             this.brsBackColorAt = new SolidBrush(this.clAtSelf);
985             this.brsBackColorYou = new SolidBrush(this.clTarget);
986             this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
987             this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
988             this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
989             this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
990
991             // StringFormatオブジェクトへの事前設定
992             this.sfTab.Alignment = StringAlignment.Center;
993             this.sfTab.LineAlignment = StringAlignment.Center;
994
995             this.InitDetailHtmlFormat();
996             this.tweetDetailsView.ClearPostBrowser();
997
998             this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]";
999
1000             this.history.Add(new StatusTextHistory());
1001             this.hisIdx = 0;
1002             this.inReplyTo = null;
1003
1004             // 各種ダイアログ設定
1005             this.SearchDialog.Owner = this;
1006             this.urlDialog.Owner = this;
1007
1008             // 新着バルーン通知のチェック状態設定
1009             this.NewPostPopMenuItem.Checked = SettingManager.Common.NewAllPop;
1010             this.NotifyFileMenuItem.Checked = this.NewPostPopMenuItem.Checked;
1011
1012             // 新着取得時のリストスクロールをするか。trueならスクロールしない
1013             this.ListLockMenuItem.Checked = SettingManager.Common.ListLock;
1014             this.LockListFileMenuItem.Checked = SettingManager.Common.ListLock;
1015             // サウンド再生(タブ別設定より優先)
1016             this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound;
1017             this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound;
1018
1019             // ウィンドウ設定
1020             this.ClientSize = ScaleBy(configScaleFactor, SettingManager.Local.FormSize);
1021             this.mySize = this.ClientSize; // サイズ保持(最小化・最大化されたまま終了した場合の対応用)
1022             this.myLoc = SettingManager.Local.FormLocation;
1023             // タイトルバー領域
1024             if (this.WindowState != FormWindowState.Minimized)
1025             {
1026                 var tbarRect = new Rectangle(this.myLoc, new Size(this.mySize.Width, SystemInformation.CaptionHeight));
1027                 var outOfScreen = true;
1028                 if (Screen.AllScreens.Length == 1) // ハングするとの報告
1029                 {
1030                     foreach (var scr in Screen.AllScreens)
1031                     {
1032                         if (!Rectangle.Intersect(tbarRect, scr.Bounds).IsEmpty)
1033                         {
1034                             outOfScreen = false;
1035                             break;
1036                         }
1037                     }
1038
1039                     if (outOfScreen)
1040                         this.myLoc = new Point(0, 0);
1041                 }
1042                 this.DesktopLocation = this.myLoc;
1043             }
1044             this.TopMost = SettingManager.Common.AlwaysTop;
1045             this.mySpDis = ScaleBy(configScaleFactor.Height, SettingManager.Local.SplitterDistance);
1046             this.mySpDis2 = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight);
1047             if (SettingManager.Local.PreviewDistance == -1)
1048             {
1049                 this.mySpDis3 = this.mySize.Width - ScaleBy(this.CurrentScaleFactor.Width, 150);
1050                 if (this.mySpDis3 < 1) this.mySpDis3 = ScaleBy(this.CurrentScaleFactor.Width, 50);
1051                 SettingManager.Local.PreviewDistance = this.mySpDis3;
1052             }
1053             else
1054             {
1055                 this.mySpDis3 = ScaleBy(configScaleFactor.Width, SettingManager.Local.PreviewDistance);
1056             }
1057             this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound;
1058             this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound;
1059             // 入力欄
1060             this.StatusText.Font = this.fntInputFont;
1061             this.StatusText.ForeColor = this.clInputFont;
1062
1063             // SplitContainer2.Panel2MinSize を一行表示の入力欄の高さに合わせる (MS UI Gothic 12pt (96dpi) の場合は 19px)
1064             this.StatusText.Multiline = false; // SettingManager.Local.StatusMultiline の設定は後で反映される
1065             this.SplitContainer2.Panel2MinSize = this.StatusText.Height;
1066
1067             // 必要であれば、発言一覧と発言詳細部・入力欄の上下を入れ替える
1068             this.SplitContainer1.IsPanelInverted = !SettingManager.Common.StatusAreaAtBottom;
1069
1070             // 全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定)
1071             if (SettingManager.Common.UnreadManage == false)
1072             {
1073                 this.ReadedStripMenuItem.Enabled = false;
1074                 this.UnreadStripMenuItem.Enabled = false;
1075             }
1076
1077             // リンク先URL表示部の初期化(画面左下)
1078             this.StatusLabelUrl.Text = "";
1079             // 状態表示部の初期化(画面右下)
1080             this.StatusLabel.Text = "";
1081             this.StatusLabel.AutoToolTip = false;
1082             this.StatusLabel.ToolTipText = "";
1083             // 文字カウンタ初期化
1084             this.lblLen.Text = this.GetRestStatusCount(this.FormatStatusTextExtended("")).ToString();
1085
1086             this.JumpReadOpMenuItem.ShortcutKeyDisplayString = "Space";
1087             this.CopySTOTMenuItem.ShortcutKeyDisplayString = "Ctrl+C";
1088             this.CopyURLMenuItem.ShortcutKeyDisplayString = "Ctrl+Shift+C";
1089             this.CopyUserIdStripMenuItem.ShortcutKeyDisplayString = "Shift+Alt+C";
1090
1091             // SourceLinkLabel のテキストが SplitContainer2.Panel2.AccessibleName にセットされるのを防ぐ
1092             // (タブオーダー順で SourceLinkLabel の次にある PostBrowser が TabStop = false となっているため、
1093             // さらに次のコントロールである SplitContainer2.Panel2 の AccessibleName がデフォルトで SourceLinkLabel のテキストになってしまう)
1094             this.SplitContainer2.Panel2.AccessibleName = "";
1095
1096             ////////////////////////////////////////////////////////////////////////////////
1097             var sortOrder = (SortOrder)SettingManager.Common.SortOrder;
1098             var mode = ComparerMode.Id;
1099             switch (SettingManager.Common.SortColumn)
1100             {
1101                 case 0: // 0:アイコン,5:未読マーク,6:プロテクト・フィルターマーク
1102                 case 5:
1103                 case 6:
1104                     // ソートしない
1105                     mode = ComparerMode.Id;  // Idソートに読み替え
1106                     break;
1107                 case 1: // ニックネーム
1108                     mode = ComparerMode.Nickname;
1109                     break;
1110                 case 2: // 本文
1111                     mode = ComparerMode.Data;
1112                     break;
1113                 case 3: // 時刻=発言Id
1114                     mode = ComparerMode.Id;
1115                     break;
1116                 case 4: // 名前
1117                     mode = ComparerMode.Name;
1118                     break;
1119                 case 7: // Source
1120                     mode = ComparerMode.Source;
1121                     break;
1122             }
1123             this.statuses.SetSortMode(mode, sortOrder);
1124             ////////////////////////////////////////////////////////////////////////////////
1125
1126             this.ApplyListViewIconSize(SettingManager.Common.IconSize);
1127
1128             // <<<<<<<<タブ関連>>>>>>>
1129             foreach (var tab in this.statuses.Tabs)
1130             {
1131                 if (!this.AddNewTab(tab, startup: true))
1132                     throw new TabException(Properties.Resources.TweenMain_LoadText1);
1133             }
1134
1135             this.statuses.SelectTab(this.ListTab.SelectedTab.Text);
1136
1137             // タブの位置を調整する
1138             this.SetTabAlignment();
1139
1140             MyCommon.TwitterApiInfo.AccessLimitUpdated += this.TwitterApiStatus_AccessLimitUpdated;
1141             Microsoft.Win32.SystemEvents.TimeChanged += this.SystemEvents_TimeChanged;
1142
1143             if (SettingManager.Common.TabIconDisp)
1144             {
1145                 this.ListTab.DrawMode = TabDrawMode.Normal;
1146             }
1147             else
1148             {
1149                 this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed;
1150                 this.ListTab.DrawItem += this.ListTab_DrawItem;
1151                 this.ListTab.ImageList = null;
1152             }
1153
1154             if (SettingManager.Common.HotkeyEnabled)
1155             {
1156                 // グローバルホットキーの登録
1157                 var modKey = HookGlobalHotkey.ModKeys.None;
1158                 if ((SettingManager.Common.HotkeyModifier & Keys.Alt) == Keys.Alt)
1159                     modKey |= HookGlobalHotkey.ModKeys.Alt;
1160                 if ((SettingManager.Common.HotkeyModifier & Keys.Control) == Keys.Control)
1161                     modKey |= HookGlobalHotkey.ModKeys.Ctrl;
1162                 if ((SettingManager.Common.HotkeyModifier & Keys.Shift) == Keys.Shift)
1163                     modKey |= HookGlobalHotkey.ModKeys.Shift;
1164                 if ((SettingManager.Common.HotkeyModifier & Keys.LWin) == Keys.LWin)
1165                     modKey |= HookGlobalHotkey.ModKeys.Win;
1166
1167                 this.hookGlobalHotkey.RegisterOriginalHotkey(SettingManager.Common.HotkeyKey, SettingManager.Common.HotkeyValue, modKey);
1168             }
1169
1170             if (SettingManager.Common.IsUseNotifyGrowl)
1171                 this.gh.RegisterGrowl();
1172
1173             this.StatusLabel.Text = Properties.Resources.Form1_LoadText1;       // 画面右下の状態表示を変更
1174
1175             this.SetMainWindowTitle();
1176             this.SetNotifyIconText();
1177
1178             if (!SettingManager.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized)
1179             {
1180                 this.Visible = true;
1181             }
1182
1183             // タイマー設定
1184
1185             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => this.RefreshTabAsync<HomeTabModel>());
1186             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Mention] = () => this.InvokeAsync(() => this.RefreshTabAsync<MentionsTabModel>());
1187             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Dm] = () => this.InvokeAsync(() => this.RefreshTabAsync<DirectMessagesTabModel>());
1188             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.PublicSearch] = () => this.InvokeAsync(() => this.RefreshTabAsync<PublicSearchTabModel>());
1189             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.User] = () => this.InvokeAsync(() => this.RefreshTabAsync<UserTimelineTabModel>());
1190             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.List] = () => this.InvokeAsync(() => this.RefreshTabAsync<ListTimelineTabModel>());
1191             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Config] = () => this.InvokeAsync(() => Task.WhenAll(new[]
1192             {
1193                 this.DoGetFollowersMenu(),
1194                 this.RefreshBlockIdsAsync(),
1195                 this.RefreshMuteUserIdsAsync(),
1196                 this.RefreshNoRetweetIdsAsync(),
1197                 this.RefreshTwitterConfigurationAsync(),
1198             }));
1199             this.RefreshTimelineScheduler();
1200
1201             this.selectionDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true);
1202             this.saveConfigDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1));
1203
1204             // 更新中アイコンアニメーション間隔
1205             this.TimerRefreshIcon.Interval = 200;
1206             this.TimerRefreshIcon.Enabled = false;
1207
1208             this.ignoreConfigSave = false;
1209             this.TweenMain_Resize(this, EventArgs.Empty);
1210             if (saveRequired) this.SaveConfigsAll(false);
1211
1212             foreach (var ua in SettingManager.Common.UserAccounts)
1213             {
1214                 if (ua.UserId == 0 && ua.Username.Equals(this.tw.Username, StringComparison.InvariantCultureIgnoreCase))
1215                 {
1216                     ua.UserId = this.tw.UserId;
1217                     break;
1218                 }
1219             }
1220
1221             if (firstRun)
1222             {
1223                 // 初回起動時だけ右下のメニューを目立たせる
1224                 this.HashStripSplitButton.ShowDropDown();
1225             }
1226         }
1227
1228         private void InitDetailHtmlFormat()
1229         {
1230             var htmlTemplate = SettingManager.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
1231
1232             this.detailHtmlFormatPreparedTemplate = htmlTemplate
1233                 .Replace("%FONT_FAMILY%", this.fntDetail.Name)
1234                 .Replace("%FONT_SIZE%", this.fntDetail.Size.ToString())
1235                 .Replace("%FONT_COLOR%", $"{this.clDetail.R},{this.clDetail.G},{this.clDetail.B}")
1236                 .Replace("%LINK_COLOR%", $"{this.clDetailLink.R},{this.clDetailLink.G},{this.clDetailLink.B}")
1237                 .Replace("%BG_COLOR%", $"{this.clDetailBackcolor.R},{this.clDetailBackcolor.G},{this.clDetailBackcolor.B}")
1238                 .Replace("%BG_REPLY_COLOR%", $"{this.clAtTo.R}, {this.clAtTo.G}, {this.clAtTo.B}");
1239         }
1240
1241         private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
1242         {
1243             string txt;
1244             try
1245             {
1246                 txt = this.statuses.Tabs[e.Index].TabName;
1247             }
1248             catch (Exception)
1249             {
1250                 return;
1251             }
1252
1253             e.Graphics.FillRectangle(System.Drawing.SystemBrushes.Control, e.Bounds);
1254             if (e.State == DrawItemState.Selected)
1255             {
1256                 e.DrawFocusRectangle();
1257             }
1258             Brush fore;
1259             try
1260             {
1261                 if (this.statuses.Tabs[txt].UnreadCount > 0)
1262                     fore = Brushes.Red;
1263                 else
1264                     fore = System.Drawing.SystemBrushes.ControlText;
1265             }
1266             catch (Exception)
1267             {
1268                 fore = System.Drawing.SystemBrushes.ControlText;
1269             }
1270             e.Graphics.DrawString(txt, e.Font, fore, e.Bounds, this.sfTab);
1271         }
1272
1273         private void LoadConfig()
1274         {
1275             SettingManager.Local = SettingManager.Local;
1276
1277             // v1.2.4 以前の設定には ScaleDimension の項目がないため、現在の DPI と同じとして扱う
1278             if (SettingManager.Local.ScaleDimension.IsEmpty)
1279                 SettingManager.Local.ScaleDimension = this.CurrentAutoScaleDimensions;
1280
1281             var tabSettings = SettingManager.Tabs;
1282             foreach (var tabSetting in tabSettings.Tabs)
1283             {
1284                 TabModel tab;
1285                 switch (tabSetting.TabType)
1286                 {
1287                     case MyCommon.TabUsageType.Home:
1288                         tab = new HomeTabModel(tabSetting.TabName);
1289                         break;
1290                     case MyCommon.TabUsageType.Mentions:
1291                         tab = new MentionsTabModel(tabSetting.TabName);
1292                         break;
1293                     case MyCommon.TabUsageType.DirectMessage:
1294                         tab = new DirectMessagesTabModel(tabSetting.TabName);
1295                         break;
1296                     case MyCommon.TabUsageType.Favorites:
1297                         tab = new FavoritesTabModel(tabSetting.TabName);
1298                         break;
1299                     case MyCommon.TabUsageType.UserDefined:
1300                         tab = new FilterTabModel(tabSetting.TabName);
1301                         break;
1302                     case MyCommon.TabUsageType.UserTimeline:
1303                         tab = new UserTimelineTabModel(tabSetting.TabName, tabSetting.User!);
1304                         break;
1305                     case MyCommon.TabUsageType.PublicSearch:
1306                         tab = new PublicSearchTabModel(tabSetting.TabName)
1307                         {
1308                             SearchWords = tabSetting.SearchWords,
1309                             SearchLang = tabSetting.SearchLang,
1310                         };
1311                         break;
1312                     case MyCommon.TabUsageType.Lists:
1313                         tab = new ListTimelineTabModel(tabSetting.TabName, tabSetting.ListInfo!);
1314                         break;
1315                     case MyCommon.TabUsageType.Mute:
1316                         tab = new MuteTabModel(tabSetting.TabName);
1317                         break;
1318                     default:
1319                         continue;
1320                 }
1321
1322                 tab.UnreadManage = tabSetting.UnreadManage;
1323                 tab.Protected = tabSetting.Protected;
1324                 tab.Notify = tabSetting.Notify;
1325                 tab.SoundFile = tabSetting.SoundFile;
1326
1327                 if (tab.IsDistributableTabType)
1328                 {
1329                     var filterTab = (FilterTabModel)tab;
1330                     filterTab.FilterArray = tabSetting.FilterArray;
1331                     filterTab.FilterModified = false;
1332                 }
1333
1334                 if (this.statuses.ContainsTab(tab.TabName))
1335                     tab.TabName = this.statuses.MakeTabName("MyTab");
1336
1337                 this.statuses.AddTab(tab);
1338             }
1339
1340             this.statuses.AddDefaultTabs();
1341         }
1342
1343         private void TimerInterval_Changed(object sender, IntervalChangedEventArgs e)
1344         {
1345             this.RefreshTimelineScheduler();
1346         }
1347
1348         private void RefreshTimelineScheduler()
1349         {
1350             static TimeSpan IntervalSecondsOrDisabled(int seconds)
1351                 => seconds == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(seconds);
1352
1353             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Home] = IntervalSecondsOrDisabled(SettingManager.Common.TimelinePeriod);
1354             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Mention] = IntervalSecondsOrDisabled(SettingManager.Common.ReplyPeriod);
1355             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Dm] = IntervalSecondsOrDisabled(SettingManager.Common.DMPeriod);
1356             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.PublicSearch] = IntervalSecondsOrDisabled(SettingManager.Common.PubSearchPeriod);
1357             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.User] = IntervalSecondsOrDisabled(SettingManager.Common.UserTimelinePeriod);
1358             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.List] = IntervalSecondsOrDisabled(SettingManager.Common.ListsPeriod);
1359             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Config] = TimeSpan.FromHours(6);
1360             this.timelineScheduler.UpdateAfterSystemResume = TimeSpan.FromSeconds(30);
1361
1362             this.timelineScheduler.RefreshSchedule();
1363         }
1364
1365         private void MarkSettingCommonModified()
1366         {
1367             if (this.saveConfigDebouncer == null)
1368                 return;
1369
1370             this.ModifySettingCommon = true;
1371             _ = this.saveConfigDebouncer.Call();
1372         }
1373
1374         private void MarkSettingLocalModified()
1375         {
1376             if (this.saveConfigDebouncer == null)
1377                 return;
1378
1379             this.ModifySettingLocal = true;
1380             _ = this.saveConfigDebouncer.Call();
1381         }
1382
1383         internal void MarkSettingAtIdModified()
1384         {
1385             if (this.saveConfigDebouncer == null)
1386                 return;
1387
1388             this.ModifySettingAtId = true;
1389             _ = this.saveConfigDebouncer.Call();
1390         }
1391
1392         private void RefreshTimeline()
1393         {
1394             var curTabModel = this.CurrentTab;
1395             var curListView = this.CurrentListView;
1396
1397             // 現在表示中のタブのスクロール位置を退避
1398             var curListScroll = this.SaveListViewScroll(curListView, curTabModel);
1399
1400             // 各タブのリスト上の選択位置などを退避
1401             var listSelections = this.SaveListViewSelection();
1402
1403             // 更新確定
1404             int addCount;
1405             addCount = this.statuses.SubmitUpdate(
1406                 out var soundFile,
1407                 out var notifyPosts,
1408                 out var newMentionOrDm,
1409                 out var isDelete);
1410
1411             if (MyCommon.EndingFlag) return;
1412
1413             // リストに反映&選択状態復元
1414             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
1415             {
1416                 var tabPage = this.ListTab.TabPages[index];
1417                 var listView = (DetailsListView)tabPage.Tag;
1418
1419                 if (listView.VirtualListSize != tab.AllCount || isDelete)
1420                 {
1421                     using (ControlTransaction.Update(listView))
1422                     {
1423                         if (listView == curListView)
1424                             this.PurgeListViewItemCache();
1425
1426                         try
1427                         {
1428                             // リスト件数更新
1429                             listView.VirtualListSize = tab.AllCount;
1430                         }
1431                         catch (NullReferenceException ex)
1432                         {
1433                             // WinForms 内部で ListView.set_TopItem が発生させている例外
1434                             // https://ja.osdn.net/ticket/browse.php?group_id=6526&tid=36588
1435                             MyCommon.TraceOut(ex, $"TabType: {tab.TabType}, Count: {tab.AllCount}, ListSize: {listView.VirtualListSize}");
1436                         }
1437
1438                         // 選択位置などを復元
1439                         this.RestoreListViewSelection(listView, tab, listSelections[tab.TabName]);
1440                     }
1441                 }
1442             }
1443
1444             if (addCount > 0)
1445             {
1446                 if (SettingManager.Common.TabIconDisp)
1447                 {
1448                     foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
1449                     {
1450                         var tabPage = this.ListTab.TabPages[index];
1451                         if (tab.UnreadCount > 0 && tabPage.ImageIndex != 0)
1452                             tabPage.ImageIndex = 0; // 未読アイコン
1453                     }
1454                 }
1455                 else
1456                 {
1457                     this.ListTab.Refresh();
1458                 }
1459             }
1460
1461             // スクロール位置を復元
1462             this.RestoreListViewScroll(curListView, curTabModel, curListScroll);
1463
1464             // 新着通知
1465             this.NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm);
1466
1467             this.SetMainWindowTitle();
1468             if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.Ordinal)) this.SetStatusLabelUrl();
1469
1470             this.HashSupl.AddRangeItem(this.tw.GetHashList());
1471         }
1472
1473         internal struct ListViewScroll
1474         {
1475             public ScrollLockMode ScrollLockMode { get; set; }
1476
1477             public long? TopItemStatusId { get; set; }
1478         }
1479
1480         internal enum ScrollLockMode
1481         {
1482             /// <summary>固定しない</summary>
1483             None,
1484
1485             /// <summary>最上部に固定する</summary>
1486             FixedToTop,
1487
1488             /// <summary>最下部に固定する</summary>
1489             FixedToBottom,
1490
1491             /// <summary><see cref="ListViewScroll.TopItemStatusId"/> の位置に固定する</summary>
1492             FixedToItem,
1493         }
1494
1495         /// <summary>
1496         /// <see cref="ListView"/> のスクロール位置に関する情報を <see cref="ListViewScroll"/> として返します
1497         /// </summary>
1498         private ListViewScroll SaveListViewScroll(DetailsListView listView, TabModel tab)
1499         {
1500             var listScroll = new ListViewScroll
1501             {
1502                 ScrollLockMode = this.GetScrollLockMode(listView),
1503             };
1504
1505             if (listScroll.ScrollLockMode == ScrollLockMode.FixedToItem)
1506             {
1507                 var topItemIndex = listView.TopItem?.Index ?? -1;
1508                 if (topItemIndex != -1 && topItemIndex < tab.AllCount)
1509                     listScroll.TopItemStatusId = tab.GetStatusIdAt(topItemIndex);
1510             }
1511
1512             return listScroll;
1513         }
1514
1515         private ScrollLockMode GetScrollLockMode(DetailsListView listView)
1516         {
1517             if (this.statuses.SortMode == ComparerMode.Id)
1518             {
1519                 if (this.statuses.SortOrder == SortOrder.Ascending)
1520                 {
1521                     // Id昇順
1522                     if (this.ListLockMenuItem.Checked)
1523                         return ScrollLockMode.None;
1524
1525                     // 最下行が表示されていたら、最下行へ強制スクロール。最下行が表示されていなかったら制御しない
1526
1527                     // 一番下に表示されているアイテム
1528                     var bottomItem = listView.GetItemAt(0, listView.ClientSize.Height - 1);
1529                     if (bottomItem == null || bottomItem.Index == listView.VirtualListSize - 1)
1530                         return ScrollLockMode.FixedToBottom;
1531                     else
1532                         return ScrollLockMode.None;
1533                 }
1534                 else
1535                 {
1536                     // Id降順
1537                     if (this.ListLockMenuItem.Checked)
1538                         return ScrollLockMode.FixedToItem;
1539
1540                     // 最上行が表示されていたら、制御しない。最上行が表示されていなかったら、現在表示位置へ強制スクロール
1541                     var topItem = listView.TopItem;
1542                     if (topItem == null || topItem.Index == 0)
1543                         return ScrollLockMode.FixedToTop;
1544                     else
1545                         return ScrollLockMode.FixedToItem;
1546                 }
1547             }
1548             else
1549             {
1550                 return ScrollLockMode.FixedToItem;
1551             }
1552         }
1553
1554         internal struct ListViewSelection
1555         {
1556             public long[]? SelectedStatusIds { get; set; }
1557
1558             public long? SelectionMarkStatusId { get; set; }
1559
1560             public long? FocusedStatusId { get; set; }
1561         }
1562
1563         /// <summary>
1564         /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
1565         /// </summary>
1566         private IReadOnlyDictionary<string, ListViewSelection> SaveListViewSelection()
1567         {
1568             var listsDict = new Dictionary<string, ListViewSelection>();
1569
1570             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
1571             {
1572                 var listView = (DetailsListView)this.ListTab.TabPages[index].Tag;
1573                 listsDict[tab.TabName] = this.SaveListViewSelection(listView, tab);
1574             }
1575
1576             return listsDict;
1577         }
1578
1579         /// <summary>
1580         /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
1581         /// </summary>
1582         private ListViewSelection SaveListViewSelection(DetailsListView listView, TabModel tab)
1583         {
1584             if (listView.VirtualListSize == 0)
1585             {
1586                 return new ListViewSelection
1587                 {
1588                     SelectedStatusIds = Array.Empty<long>(),
1589                     SelectionMarkStatusId = null,
1590                     FocusedStatusId = null,
1591                 };
1592             }
1593
1594             return new ListViewSelection
1595             {
1596                 SelectedStatusIds = tab.SelectedStatusIds,
1597                 FocusedStatusId = this.GetFocusedStatusId(listView, tab),
1598                 SelectionMarkStatusId = this.GetSelectionMarkStatusId(listView, tab),
1599             };
1600         }
1601
1602         private long? GetFocusedStatusId(DetailsListView listView, TabModel tab)
1603         {
1604             var index = listView.FocusedItem?.Index ?? -1;
1605
1606             return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null;
1607         }
1608
1609         private long? GetSelectionMarkStatusId(DetailsListView listView, TabModel tab)
1610         {
1611             var index = listView.SelectionMark;
1612
1613             return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null;
1614         }
1615
1616         /// <summary>
1617         /// <see cref="SaveListViewScroll"/> によって保存されたスクロール位置を復元します
1618         /// </summary>
1619         private void RestoreListViewScroll(DetailsListView listView, TabModel tab, ListViewScroll listScroll)
1620         {
1621             if (listView.VirtualListSize == 0)
1622                 return;
1623
1624             switch (listScroll.ScrollLockMode)
1625             {
1626                 case ScrollLockMode.FixedToTop:
1627                     listView.EnsureVisible(0);
1628                     break;
1629                 case ScrollLockMode.FixedToBottom:
1630                     listView.EnsureVisible(listView.VirtualListSize - 1);
1631                     break;
1632                 case ScrollLockMode.FixedToItem:
1633                     var topIndex = listScroll.TopItemStatusId != null ? tab.IndexOf(listScroll.TopItemStatusId.Value) : -1;
1634                     if (topIndex != -1)
1635                     {
1636                         var topItem = listView.Items[topIndex];
1637                         try
1638                         {
1639                             listView.TopItem = topItem;
1640                         }
1641                         catch (NullReferenceException)
1642                         {
1643                             listView.EnsureVisible(listView.VirtualListSize - 1);
1644                             listView.EnsureVisible(topIndex);
1645                         }
1646                     }
1647                     break;
1648                 case ScrollLockMode.None:
1649                 default:
1650                     break;
1651             }
1652         }
1653
1654         /// <summary>
1655         /// <see cref="SaveListViewSelection"/> によって保存された選択状態を復元します
1656         /// </summary>
1657         private void RestoreListViewSelection(DetailsListView listView, TabModel tab, ListViewSelection listSelection)
1658         {
1659             // status_id から ListView 上のインデックスに変換
1660             int[]? selectedIndices = null;
1661             if (listSelection.SelectedStatusIds != null)
1662                 selectedIndices = tab.IndexOf(listSelection.SelectedStatusIds).Where(x => x != -1).ToArray();
1663
1664             var focusedIndex = -1;
1665             if (listSelection.FocusedStatusId != null)
1666                 focusedIndex = tab.IndexOf(listSelection.FocusedStatusId.Value);
1667
1668             var selectionMarkIndex = -1;
1669             if (listSelection.SelectionMarkStatusId != null)
1670                 selectionMarkIndex = tab.IndexOf(listSelection.SelectionMarkStatusId.Value);
1671
1672             this.SelectListItem(listView, selectedIndices, focusedIndex, selectionMarkIndex);
1673         }
1674
1675         private bool BalloonRequired()
1676         {
1677             if (this.initial)
1678                 return false;
1679
1680             if (NativeMethods.IsScreenSaverRunning())
1681                 return false;
1682
1683             // 「新着通知」が無効
1684             if (!this.NewPostPopMenuItem.Checked)
1685                 return false;
1686
1687             // 「画面最小化・アイコン時のみバルーンを表示する」が有効
1688             if (SettingManager.Common.LimitBalloon)
1689             {
1690                 if (this.WindowState != FormWindowState.Minimized && this.Visible && Form.ActiveForm != null)
1691                     return false;
1692             }
1693
1694             return true;
1695         }
1696
1697         private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCount, bool newMentions)
1698         {
1699             if (SettingManager.Common.ReadOwnPost)
1700             {
1701                 if (notifyPosts != null && notifyPosts.Length > 0 && notifyPosts.All(x => x.UserId == this.tw.UserId))
1702                     return;
1703             }
1704
1705             // 新着通知
1706             if (this.BalloonRequired())
1707             {
1708                 if (notifyPosts != null && notifyPosts.Length > 0)
1709                 {
1710                     // Growlは一個ずつばらして通知。ただし、3ポスト以上あるときはまとめる
1711                     if (SettingManager.Common.IsUseNotifyGrowl)
1712                     {
1713                         var sb = new StringBuilder();
1714                         var reply = false;
1715                         var dm = false;
1716
1717                         foreach (var post in notifyPosts)
1718                         {
1719                             if (!(notifyPosts.Length > 3))
1720                             {
1721                                 sb.Clear();
1722                                 reply = false;
1723                                 dm = false;
1724                             }
1725                             if (post.IsReply && !post.IsExcludeReply) reply = true;
1726                             if (post.IsDm) dm = true;
1727                             if (sb.Length > 0) sb.Append(System.Environment.NewLine);
1728                             switch (SettingManager.Common.NameBalloon)
1729                             {
1730                                 case MyCommon.NameBalloonEnum.UserID:
1731                                     sb.Append(post.ScreenName).Append(" : ");
1732                                     break;
1733                                 case MyCommon.NameBalloonEnum.NickName:
1734                                     sb.Append(post.Nickname).Append(" : ");
1735                                     break;
1736                             }
1737                             sb.Append(post.TextFromApi);
1738                             if (notifyPosts.Length > 3)
1739                             {
1740                                 if (notifyPosts.Last() != post) continue;
1741                             }
1742
1743                             var title = new StringBuilder();
1744                             GrowlHelper.NotifyType nt;
1745                             if (SettingManager.Common.DispUsername)
1746                             {
1747                                 title.Append(this.tw.Username);
1748                                 title.Append(" - ");
1749                             }
1750
1751                             if (dm)
1752                             {
1753                                 title.Append(ApplicationSettings.ApplicationName);
1754                                 title.Append(" [DM] ");
1755                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1756                                 nt = GrowlHelper.NotifyType.DirectMessage;
1757                             }
1758                             else if (reply)
1759                             {
1760                                 title.Append(ApplicationSettings.ApplicationName);
1761                                 title.Append(" [Reply!] ");
1762                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1763                                 nt = GrowlHelper.NotifyType.Reply;
1764                             }
1765                             else
1766                             {
1767                                 title.Append(ApplicationSettings.ApplicationName);
1768                                 title.Append(" ");
1769                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1770                                 nt = GrowlHelper.NotifyType.Notify;
1771                             }
1772                             var bText = sb.ToString();
1773                             if (MyCommon.IsNullOrEmpty(bText)) return;
1774
1775                             var image = this.iconCache.TryGetFromCache(post.ImageUrl);
1776                             this.gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl);
1777                         }
1778                     }
1779                     else
1780                     {
1781                         var sb = new StringBuilder();
1782                         var reply = false;
1783                         var dm = false;
1784                         foreach (var post in notifyPosts)
1785                         {
1786                             if (post.IsReply && !post.IsExcludeReply) reply = true;
1787                             if (post.IsDm) dm = true;
1788                             if (sb.Length > 0) sb.Append(System.Environment.NewLine);
1789                             switch (SettingManager.Common.NameBalloon)
1790                             {
1791                                 case MyCommon.NameBalloonEnum.UserID:
1792                                     sb.Append(post.ScreenName).Append(" : ");
1793                                     break;
1794                                 case MyCommon.NameBalloonEnum.NickName:
1795                                     sb.Append(post.Nickname).Append(" : ");
1796                                     break;
1797                             }
1798                             sb.Append(post.TextFromApi);
1799                         }
1800
1801                         var title = new StringBuilder();
1802                         ToolTipIcon ntIcon;
1803                         if (SettingManager.Common.DispUsername)
1804                         {
1805                             title.Append(this.tw.Username);
1806                             title.Append(" - ");
1807                         }
1808
1809                         if (dm)
1810                         {
1811                             ntIcon = ToolTipIcon.Warning;
1812                             title.Append(ApplicationSettings.ApplicationName);
1813                             title.Append(" [DM] ");
1814                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1815                         }
1816                         else if (reply)
1817                         {
1818                             ntIcon = ToolTipIcon.Warning;
1819                             title.Append(ApplicationSettings.ApplicationName);
1820                             title.Append(" [Reply!] ");
1821                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1822                         }
1823                         else
1824                         {
1825                             ntIcon = ToolTipIcon.Info;
1826                             title.Append(ApplicationSettings.ApplicationName);
1827                             title.Append(" ");
1828                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1829                         }
1830                         var bText = sb.ToString();
1831                         if (MyCommon.IsNullOrEmpty(bText)) return;
1832
1833                         this.NotifyIcon1.BalloonTipTitle = title.ToString();
1834                         this.NotifyIcon1.BalloonTipText = bText;
1835                         this.NotifyIcon1.BalloonTipIcon = ntIcon;
1836                         this.NotifyIcon1.ShowBalloonTip(500);
1837                     }
1838                 }
1839             }
1840
1841             // サウンド再生
1842             if (!this.initial && SettingManager.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile))
1843             {
1844                 try
1845                 {
1846                     var dir = Application.StartupPath;
1847                     if (Directory.Exists(Path.Combine(dir, "Sounds")))
1848                     {
1849                         dir = Path.Combine(dir, "Sounds");
1850                     }
1851                     using var player = new SoundPlayer(Path.Combine(dir, soundFile));
1852                     player.Play();
1853                 }
1854                 catch (Exception)
1855                 {
1856                 }
1857             }
1858
1859             // mentions新着時に画面ブリンク
1860             if (!this.initial && SettingManager.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null)
1861             {
1862                 NativeMethods.FlashMyWindow(this.Handle, 3);
1863             }
1864         }
1865
1866         private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
1867         {
1868             var listView = this.CurrentListView;
1869             if (listView != sender)
1870                 return;
1871
1872             var indices = listView.SelectedIndices.Cast<int>().ToArray();
1873             this.CurrentTab.SelectPosts(indices);
1874
1875             if (indices.Length != 1)
1876                 return;
1877
1878             var index = indices[0];
1879             if (index > listView.VirtualListSize - 1) return;
1880
1881             this.PushSelectPostChain();
1882
1883             var post = this.CurrentPost!;
1884             this.statuses.SetReadAllTab(post.StatusId, read: true);
1885
1886             // キャッシュの書き換え
1887             this.ChangeCacheStyleRead(true, index); // 既読へ(フォント、文字色)
1888
1889             this.ColorizeList();
1890             await this.selectionDebouncer.Call();
1891         }
1892
1893         private void ChangeCacheStyleRead(bool read, int index)
1894         {
1895             var tabInfo = this.CurrentTab;
1896             // Read:true=既読 false=未読
1897             // 未読管理していなかったら既読として扱う
1898             if (!tabInfo.UnreadManage ||
1899                !SettingManager.Common.UnreadManage) read = true;
1900
1901             var listCache = this.listItemCache;
1902             if (listCache == null)
1903                 return;
1904
1905             // キャッシュに含まれていないアイテムは対象外
1906             if (!listCache.TryGetValue(index, out var itm, out var post))
1907                 return;
1908
1909             this.ChangeItemStyleRead(read, itm, post, (DetailsListView)listCache.TargetList);
1910         }
1911
1912         private void ChangeItemStyleRead(bool read, ListViewItem item, PostClass post, DetailsListView? dList)
1913         {
1914             Font fnt;
1915             string star;
1916             // フォント
1917             if (read)
1918             {
1919                 fnt = this.fntReaded;
1920                 star = "";
1921             }
1922             else
1923             {
1924                 fnt = this.fntUnread;
1925                 star = "★";
1926             }
1927             if (item.SubItems[5].Text != star)
1928                 item.SubItems[5].Text = star;
1929
1930             // 文字色
1931             Color cl;
1932             if (post.IsFav)
1933                 cl = this.clFav;
1934             else if (post.RetweetedId != null)
1935                 cl = this.clRetweet;
1936             else if (post.IsOwl && (post.IsDm || SettingManager.Common.OneWayLove))
1937                 cl = this.clOWL;
1938             else if (read || !SettingManager.Common.UseUnreadStyle)
1939                 cl = this.clReaded;
1940             else
1941                 cl = this.clUnread;
1942
1943             if (dList == null || item.Index == -1)
1944             {
1945                 item.ForeColor = cl;
1946                 if (SettingManager.Common.UseUnreadStyle)
1947                     item.Font = fnt;
1948             }
1949             else
1950             {
1951                 dList.Update();
1952                 if (SettingManager.Common.UseUnreadStyle)
1953                     dList.ChangeItemFontAndColor(item, cl, fnt);
1954                 else
1955                     dList.ChangeItemForeColor(item, cl);
1956             }
1957         }
1958
1959         private void ColorizeList()
1960         {
1961             // Index:更新対象のListviewItem.Index。Colorを返す。
1962             // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
1963             PostClass? post;
1964             if (this.anchorFlag)
1965                 post = this.anchorPost;
1966             else
1967                 post = this.CurrentPost;
1968
1969             if (post == null) return;
1970
1971             var listCache = this.listItemCache;
1972             if (listCache == null)
1973                 return;
1974
1975             var listView = (DetailsListView)listCache.TargetList;
1976
1977             // ValidateRectが呼ばれる前に選択色などの描画を済ませておく
1978             listView.Update();
1979
1980             foreach (var (listViewItem, cachedPost) in listCache.Cache)
1981             {
1982                 var backColor = this.JudgeColor(post, cachedPost);
1983                 listView.ChangeItemBackColor(listViewItem, backColor);
1984             }
1985         }
1986
1987         private void ColorizeList(ListViewItem item, PostClass post)
1988         {
1989             // Index:更新対象のListviewItem.Index。Colorを返す。
1990             // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
1991             PostClass? basePost;
1992             if (this.anchorFlag)
1993                 basePost = this.anchorPost;
1994             else
1995                 basePost = this.CurrentPost;
1996
1997             if (basePost == null) return;
1998
1999             if (item.Index == -1)
2000                 item.BackColor = this.JudgeColor(basePost, post);
2001             else
2002                 this.CurrentListView.ChangeItemBackColor(item, this.JudgeColor(basePost, post));
2003         }
2004
2005         private Color JudgeColor(PostClass basePost, PostClass targetPost)
2006         {
2007             Color cl;
2008             if (targetPost.StatusId == basePost.InReplyToStatusId)
2009                 // @先
2010                 cl = this.clAtTo;
2011             else if (targetPost.IsMe)
2012                 // 自分=発言者
2013                 cl = this.clSelf;
2014             else if (targetPost.IsReply)
2015                 // 自分宛返信
2016                 cl = this.clAtSelf;
2017             else if (basePost.ReplyToList.Any(x => x.UserId == targetPost.UserId))
2018                 // 返信先
2019                 cl = this.clAtFromTarget;
2020             else if (targetPost.ReplyToList.Any(x => x.UserId == basePost.UserId))
2021                 // その人への返信
2022                 cl = this.clAtTarget;
2023             else if (targetPost.UserId == basePost.UserId)
2024                 // 発言者
2025                 cl = this.clTarget;
2026             else
2027                 // その他
2028                 cl = this.clListBackcolor;
2029
2030             return cl;
2031         }
2032
2033         private void StatusTextHistoryBack()
2034         {
2035             if (!string.IsNullOrWhiteSpace(this.StatusText.Text))
2036                 this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo);
2037
2038             this.hisIdx -= 1;
2039             if (this.hisIdx < 0)
2040                 this.hisIdx = 0;
2041
2042             var historyItem = this.history[this.hisIdx];
2043             this.inReplyTo = historyItem.InReplyTo;
2044             this.StatusText.Text = historyItem.Status;
2045             this.StatusText.SelectionStart = this.StatusText.Text.Length;
2046         }
2047
2048         private void StatusTextHistoryForward()
2049         {
2050             if (!string.IsNullOrWhiteSpace(this.StatusText.Text))
2051                 this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo);
2052
2053             this.hisIdx += 1;
2054             if (this.hisIdx > this.history.Count - 1)
2055                 this.hisIdx = this.history.Count - 1;
2056
2057             var historyItem = this.history[this.hisIdx];
2058             this.inReplyTo = historyItem.InReplyTo;
2059             this.StatusText.Text = historyItem.Status;
2060             this.StatusText.SelectionStart = this.StatusText.Text.Length;
2061         }
2062
2063         private async void PostButton_Click(object sender, EventArgs e)
2064         {
2065             if (this.StatusText.Text.Trim().Length == 0)
2066             {
2067                 if (!this.ImageSelector.Enabled)
2068                 {
2069                     await this.DoRefresh();
2070                     return;
2071                 }
2072             }
2073
2074             var currentPost = this.CurrentPost;
2075             if (this.ExistCurrentPost && currentPost != null && this.StatusText.Text.Trim() == string.Format("RT @{0}: {1}", currentPost.ScreenName, currentPost.TextFromApi))
2076             {
2077                 var rtResult = MessageBox.Show(string.Format(Properties.Resources.PostButton_Click1, Environment.NewLine),
2078                                                                "Retweet",
2079                                                                MessageBoxButtons.YesNoCancel,
2080                                                                MessageBoxIcon.Question);
2081                 switch (rtResult)
2082                 {
2083                     case DialogResult.Yes:
2084                         this.StatusText.Text = "";
2085                         await this.DoReTweetOfficial(false);
2086                         return;
2087                     case DialogResult.Cancel:
2088                         return;
2089                 }
2090             }
2091
2092             if (TextContainsOnlyMentions(this.StatusText.Text))
2093             {
2094                 var message = string.Format(Properties.Resources.PostConfirmText, this.StatusText.Text);
2095                 var ret = MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
2096
2097                 if (ret != DialogResult.OK)
2098                     return;
2099             }
2100
2101             this.history[this.history.Count - 1] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo);
2102
2103             if (SettingManager.Common.Nicoms)
2104             {
2105                 this.StatusText.SelectionStart = this.StatusText.Text.Length;
2106                 await this.UrlConvertAsync(MyCommon.UrlConverter.Nicoms);
2107             }
2108
2109             this.StatusText.SelectionStart = this.StatusText.Text.Length;
2110             this.CheckReplyTo(this.StatusText.Text);
2111
2112             var status = new PostStatusParams();
2113
2114             var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
2115             if (this.GetRestStatusCount(statusTextCompat) >= 0)
2116             {
2117                 // auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
2118                 // 収まる場合はこれらのオプションを使用せずに投稿する
2119                 status.Text = statusTextCompat;
2120                 status.InReplyToStatusId = this.inReplyTo?.StatusId;
2121             }
2122             else
2123             {
2124                 status.Text = this.FormatStatusTextExtended(this.StatusText.Text, out var autoPopulatedUserIds, out var attachmentUrl);
2125                 status.InReplyToStatusId = this.inReplyTo?.StatusId;
2126
2127                 status.AttachmentUrl = attachmentUrl;
2128
2129                 // リプライ先がセットされていても autoPopulatedUserIds が空の場合は auto_populate_reply_metadata を有効にしない
2130                 //  (非公式 RT の場合など)
2131                 var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null;
2132                 if (replyToPost != null && autoPopulatedUserIds.Length != 0)
2133                 {
2134                     status.AutoPopulateReplyMetadata = true;
2135
2136                     // ReplyToList のうち autoPopulatedUserIds に含まれていないユーザー ID を抽出
2137                     status.ExcludeReplyUserIds = replyToPost.ReplyToList.Select(x => x.UserId).Except(autoPopulatedUserIds)
2138                         .ToArray();
2139                 }
2140             }
2141
2142             if (this.GetRestStatusCount(status.Text) < 0)
2143             {
2144                 // 文字数制限を超えているが強制的に投稿するか
2145                 var ret = MessageBox.Show(Properties.Resources.PostLengthOverMessage1, Properties.Resources.PostLengthOverMessage2, MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
2146                 if (ret != DialogResult.OK)
2147                     return;
2148             }
2149
2150             IMediaUploadService? uploadService = null;
2151             IMediaItem[]? uploadItems = null;
2152             if (this.ImageSelector.Visible)
2153             {
2154                 // 画像投稿
2155                 if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
2156                     return;
2157
2158                 uploadService = this.ImageSelector.GetService(serviceName);
2159             }
2160
2161             this.inReplyTo = null;
2162             this.StatusText.Text = "";
2163             this.history.Add(new StatusTextHistory());
2164             this.hisIdx = this.history.Count - 1;
2165             if (!SettingManager.Common.FocusLockToStatusText)
2166                 this.CurrentListView.Focus();
2167             this.urlUndoBuffer = null;
2168             this.UrlUndoToolStripMenuItem.Enabled = false;  // Undoをできないように設定
2169
2170             // Google検索(試験実装)
2171             if (this.StatusText.Text.StartsWith("Google:", StringComparison.OrdinalIgnoreCase) && this.StatusText.Text.Trim().Length > 7)
2172             {
2173                 var tmp = string.Format(Properties.Resources.SearchItem2Url, Uri.EscapeDataString(this.StatusText.Text.Substring(7)));
2174                 await MyCommon.OpenInBrowserAsync(this, tmp);
2175             }
2176
2177             await this.PostMessageAsync(status, uploadService, uploadItems);
2178         }
2179
2180         private void EndToolStripMenuItem_Click(object sender, EventArgs e)
2181         {
2182             MyCommon.EndingFlag = true;
2183             this.Close();
2184         }
2185
2186         private void TweenMain_FormClosing(object sender, FormClosingEventArgs e)
2187         {
2188             if (!SettingManager.Common.CloseToExit && e.CloseReason == CloseReason.UserClosing && MyCommon.EndingFlag == false)
2189             {
2190                 // _endingFlag=false:フォームの×ボタン
2191                 e.Cancel = true;
2192                 this.Visible = false;
2193             }
2194             else
2195             {
2196                 this.hookGlobalHotkey.UnregisterAllOriginalHotkey();
2197                 this.ignoreConfigSave = true;
2198                 MyCommon.EndingFlag = true;
2199                 this.timelineScheduler.Enabled = false;
2200                 this.TimerRefreshIcon.Enabled = false;
2201             }
2202         }
2203
2204         private void NotifyIcon1_BalloonTipClicked(object sender, EventArgs e)
2205         {
2206             this.Visible = true;
2207             if (this.WindowState == FormWindowState.Minimized)
2208             {
2209                 this.WindowState = FormWindowState.Normal;
2210             }
2211             this.Activate();
2212             this.BringToFront();
2213         }
2214
2215         private static int errorCount = 0;
2216
2217         private static bool CheckAccountValid()
2218         {
2219             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2220             {
2221                 errorCount += 1;
2222                 if (errorCount > 5)
2223                 {
2224                     errorCount = 0;
2225                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2226                     return true;
2227                 }
2228                 return false;
2229             }
2230             errorCount = 0;
2231             return true;
2232         }
2233
2234         /// <summary>指定された型 <typeparamref name="T"/> に合致する全てのタブを更新します</summary>
2235         private Task RefreshTabAsync<T>()
2236             where T : TabModel
2237             => this.RefreshTabAsync<T>(backward: false);
2238
2239         /// <summary>指定された型 <typeparamref name="T"/> に合致する全てのタブを更新します</summary>
2240         private Task RefreshTabAsync<T>(bool backward)
2241             where T : TabModel
2242         {
2243             var loadTasks =
2244                 from tab in this.statuses.GetTabsByType<T>()
2245                 select this.RefreshTabAsync(tab, backward);
2246
2247             return Task.WhenAll(loadTasks);
2248         }
2249
2250         /// <summary>指定されたタブ <paramref name="tab"/> を更新します</summary>
2251         private Task RefreshTabAsync(TabModel tab)
2252             => this.RefreshTabAsync(tab, backward: false);
2253
2254         /// <summary>指定されたタブ <paramref name="tab"/> を更新します</summary>
2255         private async Task RefreshTabAsync(TabModel tab, bool backward)
2256         {
2257             await this.workerSemaphore.WaitAsync();
2258
2259             try
2260             {
2261                 this.RefreshTasktrayIcon();
2262                 await Task.Run(() => tab.RefreshAsync(this.tw, backward, this.initial, this.workerProgress));
2263             }
2264             catch (WebApiException ex)
2265             {
2266                 this.myStatusError = true;
2267                 var tabType = tab switch
2268                 {
2269                     HomeTabModel _ => "GetTimeline",
2270                     MentionsTabModel _ => "GetTimeline",
2271                     DirectMessagesTabModel _ => "GetDirectMessage",
2272                     FavoritesTabModel _ => "GetFavorites",
2273                     PublicSearchTabModel _ => "GetSearch",
2274                     UserTimelineTabModel _ => "GetUserTimeline",
2275                     ListTimelineTabModel _ => "GetListStatus",
2276                     RelatedPostsTabModel _ => "GetRelatedTweets",
2277                     _ => tab.GetType().Name.Replace("Model", ""),
2278                 };
2279                 this.StatusLabel.Text = $"Err:{ex.Message}({tabType})";
2280             }
2281             finally
2282             {
2283                 this.RefreshTimeline();
2284                 this.workerSemaphore.Release();
2285             }
2286         }
2287
2288         private async Task FavAddAsync(long statusId, TabModel tab)
2289         {
2290             await this.workerSemaphore.WaitAsync();
2291
2292             try
2293             {
2294                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
2295
2296                 this.RefreshTasktrayIcon();
2297                 await this.FavAddAsyncInternal(progress, this.workerCts.Token, statusId, tab);
2298             }
2299             catch (WebApiException ex)
2300             {
2301                 this.myStatusError = true;
2302                 this.StatusLabel.Text = $"Err:{ex.Message}(PostFavAdd)";
2303             }
2304             finally
2305             {
2306                 this.workerSemaphore.Release();
2307             }
2308         }
2309
2310         private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, long statusId, TabModel tab)
2311         {
2312             if (ct.IsCancellationRequested)
2313                 return;
2314
2315             if (!CheckAccountValid())
2316                 throw new WebApiException("Auth error. Check your account");
2317
2318             if (!tab.Posts.TryGetValue(statusId, out var post))
2319                 return;
2320
2321             if (post.IsFav)
2322                 return;
2323
2324             await Task.Run(async () =>
2325             {
2326                 p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 0, 1, 0));
2327
2328                 try
2329                 {
2330                     try
2331                     {
2332                         await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId)
2333                             .IgnoreResponse()
2334                             .ConfigureAwait(false);
2335                     }
2336                     catch (TwitterApiException ex)
2337                         when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited))
2338                     {
2339                         // エラーコード 139 のみの場合は成功と見なす
2340                     }
2341
2342                     if (SettingManager.Common.RestrictFavCheck)
2343                     {
2344                         var status = await this.twitterApi.StatusesShow(post.RetweetedId ?? post.StatusId)
2345                             .ConfigureAwait(false);
2346
2347                         if (status.Favorited != true)
2348                             throw new WebApiException("NG(Restricted?)");
2349                     }
2350
2351                     this.favTimestamps.Add(DateTimeUtc.Now);
2352
2353                     // TLでも取得済みならfav反映
2354                     if (this.statuses.Posts.TryGetValue(statusId, out var postTl))
2355                     {
2356                         postTl.IsFav = true;
2357
2358                         var favTab = this.statuses.FavoriteTab;
2359                         favTab.AddPostQueue(postTl);
2360                     }
2361
2362                     // 検索,リスト,UserTimeline,Relatedの各タブに反映
2363                     foreach (var tb in this.statuses.GetTabsInnerStorageType())
2364                     {
2365                         if (tb.Contains(statusId))
2366                             tb.Posts[statusId].IsFav = true;
2367                     }
2368
2369                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 1, 1, 0));
2370                 }
2371                 catch (WebApiException)
2372                 {
2373                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 1, 1, 1));
2374                     throw;
2375                 }
2376
2377                 // 時速表示用
2378                 var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
2379                 foreach (var i in MyCommon.CountDown(this.favTimestamps.Count - 1, 0))
2380                 {
2381                     if (this.favTimestamps[i] < oneHour)
2382                         this.favTimestamps.RemoveAt(i);
2383                 }
2384
2385                 this.statuses.DistributePosts();
2386             });
2387
2388             if (ct.IsCancellationRequested)
2389                 return;
2390
2391             this.RefreshTimeline();
2392
2393             if (this.CurrentTabName == tab.TabName)
2394             {
2395                 using (ControlTransaction.Update(this.CurrentListView))
2396                 {
2397                     var idx = tab.IndexOf(statusId);
2398                     if (idx != -1)
2399                         this.ChangeCacheStyleRead(post.IsRead, idx);
2400                 }
2401
2402                 var currentPost = this.CurrentPost;
2403                 if (currentPost != null && statusId == currentPost.StatusId)
2404                     this.DispSelectedPost(true); // 選択アイテム再表示
2405             }
2406         }
2407
2408         private async Task FavRemoveAsync(IReadOnlyList<long> statusIds, TabModel tab)
2409         {
2410             await this.workerSemaphore.WaitAsync();
2411
2412             try
2413             {
2414                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
2415
2416                 this.RefreshTasktrayIcon();
2417                 await this.FavRemoveAsyncInternal(progress, this.workerCts.Token, statusIds, tab);
2418             }
2419             catch (WebApiException ex)
2420             {
2421                 this.myStatusError = true;
2422                 this.StatusLabel.Text = $"Err:{ex.Message}(PostFavRemove)";
2423             }
2424             finally
2425             {
2426                 this.workerSemaphore.Release();
2427             }
2428         }
2429
2430         private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds, TabModel tab)
2431         {
2432             if (ct.IsCancellationRequested)
2433                 return;
2434
2435             if (!CheckAccountValid())
2436                 throw new WebApiException("Auth error. Check your account");
2437
2438             var successIds = new List<long>();
2439
2440             await Task.Run(async () =>
2441             {
2442                 // スレッド処理はしない
2443                 var allCount = 0;
2444                 var failedCount = 0;
2445                 foreach (var statusId in statusIds)
2446                 {
2447                     allCount++;
2448
2449                     var post = tab.Posts[statusId];
2450
2451                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText17, allCount, statusIds.Count, failedCount));
2452
2453                     if (!post.IsFav)
2454                         continue;
2455
2456                     try
2457                     {
2458                         await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId)
2459                             .IgnoreResponse()
2460                             .ConfigureAwait(false);
2461                     }
2462                     catch (WebApiException)
2463                     {
2464                         failedCount++;
2465                         continue;
2466                     }
2467
2468                     successIds.Add(statusId);
2469                     post.IsFav = false; // リスト再描画必要
2470
2471                     if (this.statuses.Posts.TryGetValue(statusId, out var tabinfoPost))
2472                         tabinfoPost.IsFav = false;
2473
2474                     // 検索,リスト,UserTimeline,Relatedの各タブに反映
2475                     foreach (var tb in this.statuses.GetTabsInnerStorageType())
2476                     {
2477                         if (tb.Contains(statusId))
2478                             tb.Posts[statusId].IsFav = false;
2479                     }
2480                 }
2481             });
2482
2483             if (ct.IsCancellationRequested)
2484                 return;
2485
2486             var favTab = this.statuses.FavoriteTab;
2487             foreach (var statusId in successIds)
2488             {
2489                 // ツイートが削除された訳ではないので IsDeleted はセットしない
2490                 favTab.EnqueueRemovePost(statusId, setIsDeleted: false);
2491             }
2492
2493             this.RefreshTimeline();
2494
2495             if (this.CurrentTabName == tab.TabName)
2496             {
2497                 if (tab.TabType == MyCommon.TabUsageType.Favorites)
2498                 {
2499                     // 色変えは不要
2500                 }
2501                 else
2502                 {
2503                     using (ControlTransaction.Update(this.CurrentListView))
2504                     {
2505                         foreach (var statusId in successIds)
2506                         {
2507                             var idx = tab.IndexOf(statusId);
2508                             if (idx == -1)
2509                                 continue;
2510
2511                             var post = tab.Posts[statusId];
2512                             this.ChangeCacheStyleRead(post.IsRead, idx);
2513                         }
2514                     }
2515
2516                     var currentPost = this.CurrentPost;
2517                     if (currentPost != null && successIds.Contains(currentPost.StatusId))
2518                         this.DispSelectedPost(true); // 選択アイテム再表示
2519                 }
2520             }
2521         }
2522
2523         private async Task PostMessageAsync(PostStatusParams postParams, IMediaUploadService? uploadService, IMediaItem[]? uploadItems)
2524         {
2525             await this.workerSemaphore.WaitAsync();
2526
2527             try
2528             {
2529                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
2530
2531                 this.RefreshTasktrayIcon();
2532                 await this.PostMessageAsyncInternal(progress, this.workerCts.Token, postParams, uploadService, uploadItems);
2533             }
2534             catch (WebApiException ex)
2535             {
2536                 this.myStatusError = true;
2537                 this.StatusLabel.Text = $"Err:{ex.Message}(PostMessage)";
2538             }
2539             finally
2540             {
2541                 this.workerSemaphore.Release();
2542             }
2543         }
2544
2545         private async Task PostMessageAsyncInternal(
2546             IProgress<string> p,
2547             CancellationToken ct,
2548             PostStatusParams postParams,
2549             IMediaUploadService? uploadService,
2550             IMediaItem[]? uploadItems)
2551         {
2552             if (ct.IsCancellationRequested)
2553                 return;
2554
2555             if (!CheckAccountValid())
2556                 throw new WebApiException("Auth error. Check your account");
2557
2558             p.Report("Posting...");
2559
2560             PostClass? post = null;
2561             var errMsg = "";
2562
2563             try
2564             {
2565                 await Task.Run(async () =>
2566                 {
2567                     var postParamsWithMedia = postParams;
2568
2569                     if (uploadService != null && uploadItems != null && uploadItems.Length > 0)
2570                     {
2571                         postParamsWithMedia = await uploadService.UploadAsync(uploadItems, postParamsWithMedia)
2572                             .ConfigureAwait(false);
2573                     }
2574
2575                     post = await this.tw.PostStatus(postParamsWithMedia)
2576                         .ConfigureAwait(false);
2577                 });
2578
2579                 p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4);
2580             }
2581             catch (WebApiException ex)
2582             {
2583                 // 処理は中断せずエラーの表示のみ行う
2584                 errMsg = $"Err:{ex.Message}(PostMessage)";
2585                 p.Report(errMsg);
2586                 this.myStatusError = true;
2587             }
2588             catch (UnauthorizedAccessException ex)
2589             {
2590                 // アップロード対象のファイルが開けなかった場合など
2591                 errMsg = $"Err:{ex.Message}(PostMessage)";
2592                 p.Report(errMsg);
2593                 this.myStatusError = true;
2594             }
2595             finally
2596             {
2597                 // 使い終わった MediaItem は破棄する
2598                 if (uploadItems != null)
2599                 {
2600                     foreach (var disposableItem in uploadItems.OfType<IDisposable>())
2601                     {
2602                         disposableItem.Dispose();
2603                     }
2604                 }
2605             }
2606
2607             if (ct.IsCancellationRequested)
2608                 return;
2609
2610             if (!MyCommon.IsNullOrEmpty(errMsg) &&
2611                 !errMsg.StartsWith("OK:", StringComparison.Ordinal) &&
2612                 !errMsg.StartsWith("Warn:", StringComparison.Ordinal))
2613             {
2614                 var message = string.Format(Properties.Resources.StatusUpdateFailed, errMsg, postParams.Text);
2615
2616                 var ret = MessageBox.Show(
2617                     message,
2618                     "Failed to update status",
2619                     MessageBoxButtons.RetryCancel,
2620                     MessageBoxIcon.Question);
2621
2622                 if (ret == DialogResult.Retry)
2623                 {
2624                     await this.PostMessageAsync(postParams, uploadService, uploadItems);
2625                 }
2626                 else
2627                 {
2628                     this.StatusTextHistoryBack();
2629                     this.StatusText.Focus();
2630
2631                     // 連投モードのときだけEnterイベントが起きないので強制的に背景色を戻す
2632                     if (SettingManager.Common.FocusLockToStatusText)
2633                         this.StatusText_Enter(this.StatusText, EventArgs.Empty);
2634                 }
2635                 return;
2636             }
2637
2638             this.postTimestamps.Add(DateTimeUtc.Now);
2639
2640             var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
2641             foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0))
2642             {
2643                 if (this.postTimestamps[i] < oneHour)
2644                     this.postTimestamps.RemoveAt(i);
2645             }
2646
2647             if (!this.HashMgr.IsPermanent && !MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
2648             {
2649                 this.HashMgr.ClearHashtag();
2650                 this.HashStripSplitButton.Text = "#[-]";
2651                 this.HashTogglePullDownMenuItem.Checked = false;
2652                 this.HashToggleMenuItem.Checked = false;
2653             }
2654
2655             this.SetMainWindowTitle();
2656
2657             // TLに反映
2658             if (SettingManager.Common.PostAndGet)
2659             {
2660                 await this.RefreshTabAsync<HomeTabModel>();
2661             }
2662             else
2663             {
2664                 if (post != null)
2665                 {
2666                     this.statuses.AddPost(post);
2667                     this.statuses.DistributePosts();
2668                 }
2669                 this.RefreshTimeline();
2670             }
2671         }
2672
2673         private async Task RetweetAsync(IReadOnlyList<long> statusIds)
2674         {
2675             await this.workerSemaphore.WaitAsync();
2676
2677             try
2678             {
2679                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
2680
2681                 this.RefreshTasktrayIcon();
2682                 await this.RetweetAsyncInternal(progress, this.workerCts.Token, statusIds);
2683             }
2684             catch (WebApiException ex)
2685             {
2686                 this.myStatusError = true;
2687                 this.StatusLabel.Text = $"Err:{ex.Message}(PostRetweet)";
2688             }
2689             finally
2690             {
2691                 this.workerSemaphore.Release();
2692             }
2693         }
2694
2695         private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds)
2696         {
2697             if (ct.IsCancellationRequested)
2698                 return;
2699
2700             if (!CheckAccountValid())
2701                 throw new WebApiException("Auth error. Check your account");
2702
2703             bool read;
2704             if (!SettingManager.Common.UnreadManage)
2705                 read = true;
2706             else
2707                 read = this.initial && SettingManager.Common.Read;
2708
2709             p.Report("Posting...");
2710
2711             var posts = new List<PostClass>();
2712
2713             await Task.Run(async () =>
2714             {
2715                 foreach (var statusId in statusIds)
2716                 {
2717                     var post = await this.tw.PostRetweet(statusId, read).ConfigureAwait(false);
2718                     if (post != null) posts.Add(post);
2719                 }
2720             });
2721
2722             if (ct.IsCancellationRequested)
2723                 return;
2724
2725             p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4);
2726
2727             this.postTimestamps.Add(DateTimeUtc.Now);
2728
2729             var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
2730             foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0))
2731             {
2732                 if (this.postTimestamps[i] < oneHour)
2733                     this.postTimestamps.RemoveAt(i);
2734             }
2735
2736             // 自分のRTはTLの更新では取得できない場合があるので、
2737             // 投稿時取得の有無に関わらず追加しておく
2738             posts.ForEach(post => this.statuses.AddPost(post));
2739
2740             if (SettingManager.Common.PostAndGet)
2741             {
2742                 await this.RefreshTabAsync<HomeTabModel>();
2743             }
2744             else
2745             {
2746                 this.statuses.DistributePosts();
2747                 this.RefreshTimeline();
2748             }
2749         }
2750
2751         private async Task RefreshFollowerIdsAsync()
2752         {
2753             await this.workerSemaphore.WaitAsync();
2754
2755             try
2756             {
2757                 this.RefreshTasktrayIcon();
2758                 this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText1;
2759
2760                 await this.tw.RefreshFollowerIds();
2761
2762                 this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText3;
2763
2764                 this.RefreshTimeline();
2765                 this.PurgeListViewItemCache();
2766                 this.CurrentListView.Refresh();
2767             }
2768             catch (WebApiException ex)
2769             {
2770                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshFollowersIds)";
2771             }
2772             finally
2773             {
2774                 this.workerSemaphore.Release();
2775             }
2776         }
2777
2778         private async Task RefreshNoRetweetIdsAsync()
2779         {
2780             await this.workerSemaphore.WaitAsync();
2781
2782             try
2783             {
2784                 this.RefreshTasktrayIcon();
2785                 await this.tw.RefreshNoRetweetIds();
2786
2787                 this.StatusLabel.Text = "NoRetweetIds refreshed";
2788             }
2789             catch (WebApiException ex)
2790             {
2791                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshNoRetweetIds)";
2792             }
2793             finally
2794             {
2795                 this.workerSemaphore.Release();
2796             }
2797         }
2798
2799         private async Task RefreshBlockIdsAsync()
2800         {
2801             await this.workerSemaphore.WaitAsync();
2802
2803             try
2804             {
2805                 this.RefreshTasktrayIcon();
2806                 this.StatusLabel.Text = Properties.Resources.UpdateBlockUserText1;
2807
2808                 await this.tw.RefreshBlockIds();
2809
2810                 this.StatusLabel.Text = Properties.Resources.UpdateBlockUserText3;
2811             }
2812             catch (WebApiException ex)
2813             {
2814                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshBlockIds)";
2815             }
2816             finally
2817             {
2818                 this.workerSemaphore.Release();
2819             }
2820         }
2821
2822         private async Task RefreshTwitterConfigurationAsync()
2823         {
2824             await this.workerSemaphore.WaitAsync();
2825
2826             try
2827             {
2828                 this.RefreshTasktrayIcon();
2829                 await this.tw.RefreshConfiguration();
2830
2831                 if (this.tw.Configuration.PhotoSizeLimit != 0)
2832                 {
2833                     foreach (var service in this.ImageSelector.GetServices())
2834                     {
2835                         service.UpdateTwitterConfiguration(this.tw.Configuration);
2836                     }
2837                 }
2838
2839                 this.PurgeListViewItemCache();
2840                 this.CurrentListView.Refresh();
2841             }
2842             catch (WebApiException ex)
2843             {
2844                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshConfiguration)";
2845             }
2846             finally
2847             {
2848                 this.workerSemaphore.Release();
2849             }
2850         }
2851
2852         private async Task RefreshMuteUserIdsAsync()
2853         {
2854             this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Start;
2855
2856             try
2857             {
2858                 await this.tw.RefreshMuteUserIdsAsync();
2859             }
2860             catch (WebApiException ex)
2861             {
2862                 this.StatusLabel.Text = string.Format(Properties.Resources.UpdateMuteUserIds_Error, ex.Message);
2863                 return;
2864             }
2865
2866             this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Finish;
2867         }
2868
2869         private void NotifyIcon1_MouseClick(object sender, MouseEventArgs e)
2870         {
2871             if (e.Button == MouseButtons.Left)
2872             {
2873                 this.Visible = true;
2874                 if (this.WindowState == FormWindowState.Minimized)
2875                 {
2876                     this.WindowState = this.formWindowState;
2877                 }
2878                 this.Activate();
2879                 this.BringToFront();
2880             }
2881         }
2882
2883         private async void MyList_MouseDoubleClick(object sender, MouseEventArgs e)
2884             => await this.ListItemDoubleClickAction();
2885
2886         private async Task ListItemDoubleClickAction()
2887         {
2888             switch (SettingManager.Common.ListDoubleClickAction)
2889             {
2890                 case MyCommon.ListItemDoubleClickActionType.Reply:
2891                     this.MakeReplyText();
2892                     break;
2893                 case MyCommon.ListItemDoubleClickActionType.Favorite:
2894                     await this.FavoriteChange(true);
2895                     break;
2896                 case MyCommon.ListItemDoubleClickActionType.ShowProfile:
2897                     var post = this.CurrentPost;
2898                     if (post != null)
2899                         await this.ShowUserStatus(post.ScreenName, false);
2900                     break;
2901                 case MyCommon.ListItemDoubleClickActionType.ShowTimeline:
2902                     await this.ShowUserTimeline();
2903                     break;
2904                 case MyCommon.ListItemDoubleClickActionType.ShowRelated:
2905                     this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
2906                     break;
2907                 case MyCommon.ListItemDoubleClickActionType.OpenHomeInBrowser:
2908                     this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty);
2909                     break;
2910                 case MyCommon.ListItemDoubleClickActionType.OpenStatusInBrowser:
2911                     this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty);
2912                     break;
2913                 case MyCommon.ListItemDoubleClickActionType.None:
2914                 default:
2915                     // 動作なし
2916                     break;
2917             }
2918         }
2919
2920         private async void FavAddToolStripMenuItem_Click(object sender, EventArgs e)
2921             => await this.FavoriteChange(true);
2922
2923         private async void FavRemoveToolStripMenuItem_Click(object sender, EventArgs e)
2924             => await this.FavoriteChange(false);
2925
2926         private async void FavoriteRetweetMenuItem_Click(object sender, EventArgs e)
2927             => await this.FavoritesRetweetOfficial();
2928
2929         private async void FavoriteRetweetUnofficialMenuItem_Click(object sender, EventArgs e)
2930             => await this.FavoritesRetweetUnofficial();
2931
2932         private async Task FavoriteChange(bool favAdd, bool multiFavoriteChangeDialogEnable = true)
2933         {
2934             var tab = this.CurrentTab;
2935             var posts = tab.SelectedPosts;
2936
2937             // trueでFavAdd,falseでFavRemove
2938             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || posts.Length == 0
2939                 || !this.ExistCurrentPost) return;
2940
2941             if (posts.Length > 1)
2942             {
2943                 if (favAdd)
2944                 {
2945                     // 複数ツイートの一括ふぁぼは禁止
2946                     // https://support.twitter.com/articles/76915#favoriting
2947                     MessageBox.Show(string.Format(Properties.Resources.FavoriteLimitCountText, 1));
2948                     this.doFavRetweetFlags = false;
2949                     return;
2950                 }
2951                 else
2952                 {
2953                     if (multiFavoriteChangeDialogEnable)
2954                     {
2955                         var confirm = MessageBox.Show(
2956                             Properties.Resources.FavRemoveToolStripMenuItem_ClickText1,
2957                             Properties.Resources.FavRemoveToolStripMenuItem_ClickText2,
2958                             MessageBoxButtons.OKCancel,
2959                             MessageBoxIcon.Question);
2960
2961                         if (confirm == DialogResult.Cancel)
2962                             return;
2963                     }
2964                 }
2965             }
2966
2967             if (favAdd)
2968             {
2969                 var selectedPost = posts.Single();
2970                 if (selectedPost.IsFav)
2971                 {
2972                     this.StatusLabel.Text = Properties.Resources.FavAddToolStripMenuItem_ClickText4;
2973                     return;
2974                 }
2975
2976                 await this.FavAddAsync(selectedPost.StatusId, tab);
2977             }
2978             else
2979             {
2980                 var selectedPosts = posts.Where(x => x.IsFav);
2981                 var statusIds = selectedPosts.Select(x => x.StatusId).ToArray();
2982                 if (statusIds.Length == 0)
2983                 {
2984                     this.StatusLabel.Text = Properties.Resources.FavRemoveToolStripMenuItem_ClickText4;
2985                     return;
2986                 }
2987
2988                 await this.FavRemoveAsync(statusIds, tab);
2989             }
2990         }
2991
2992         private PostClass GetCurTabPost(int index)
2993         {
2994             var listCache = this.listItemCache;
2995             if (listCache != null)
2996             {
2997                 if (listCache.TryGetValue(index, out _, out var post))
2998                     return post;
2999             }
3000
3001             return this.CurrentTab[index];
3002         }
3003
3004         private async void AuthorOpenInBrowserMenuItem_Click(object sender, EventArgs e)
3005         {
3006             var post = this.CurrentPost;
3007             if (post != null)
3008                 await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + post.ScreenName);
3009             else
3010                 await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl);
3011         }
3012
3013         private void TweenMain_ClientSizeChanged(object sender, EventArgs e)
3014         {
3015             if ((!this.initialLayout) && this.Visible)
3016             {
3017                 if (this.WindowState == FormWindowState.Normal)
3018                 {
3019                     this.mySize = this.ClientSize;
3020                     this.mySpDis = this.SplitContainer1.SplitterDistance;
3021                     this.mySpDis3 = this.SplitContainer3.SplitterDistance;
3022                     if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height;
3023                     this.MarkSettingLocalModified();
3024                 }
3025             }
3026         }
3027
3028         private void MyList_ColumnClick(object sender, ColumnClickEventArgs e)
3029         {
3030             var comparerMode = this.GetComparerModeByColumnIndex(e.Column);
3031             if (comparerMode == null)
3032                 return;
3033
3034             this.SetSortColumn(comparerMode.Value);
3035         }
3036
3037         /// <summary>
3038         /// 列インデックスからソートを行う ComparerMode を求める
3039         /// </summary>
3040         /// <param name="columnIndex">ソートを行うカラムのインデックス (表示上の順序とは異なる)</param>
3041         /// <returns>ソートを行う ComparerMode。null であればソートを行わない</returns>
3042         private ComparerMode? GetComparerModeByColumnIndex(int columnIndex)
3043         {
3044             if (this.iconCol)
3045                 return ComparerMode.Id;
3046
3047             return columnIndex switch
3048             {
3049                 1 => ComparerMode.Nickname, // ニックネーム
3050                 2 => ComparerMode.Data, // 本文
3051                 3 => ComparerMode.Id, // 時刻=発言Id
3052                 4 => ComparerMode.Name, // 名前
3053                 7 => ComparerMode.Source, // Source
3054                 _ => (ComparerMode?)null, // 0:アイコン, 5:未読マーク, 6:プロテクト・フィルターマーク
3055             };
3056         }
3057
3058         /// <summary>
3059         /// 発言一覧の指定した位置の列でソートする
3060         /// </summary>
3061         /// <param name="columnIndex">ソートする列の位置 (表示上の順序で指定)</param>
3062         private void SetSortColumnByDisplayIndex(int columnIndex)
3063         {
3064             // 表示上の列の位置から ColumnHeader を求める
3065             var col = this.CurrentListView.Columns.Cast<ColumnHeader>()
3066                 .FirstOrDefault(x => x.DisplayIndex == columnIndex);
3067
3068             if (col == null)
3069                 return;
3070
3071             var comparerMode = this.GetComparerModeByColumnIndex(col.Index);
3072             if (comparerMode == null)
3073                 return;
3074
3075             this.SetSortColumn(comparerMode.Value);
3076         }
3077
3078         /// <summary>
3079         /// 発言一覧の最後列の項目でソートする
3080         /// </summary>
3081         private void SetSortLastColumn()
3082         {
3083             // 表示上の最後列にある ColumnHeader を求める
3084             var col = this.CurrentListView.Columns.Cast<ColumnHeader>()
3085                 .OrderByDescending(x => x.DisplayIndex)
3086                 .First();
3087
3088             var comparerMode = this.GetComparerModeByColumnIndex(col.Index);
3089             if (comparerMode == null)
3090                 return;
3091
3092             this.SetSortColumn(comparerMode.Value);
3093         }
3094
3095         /// <summary>
3096         /// 発言一覧を指定された ComparerMode に基づいてソートする
3097         /// </summary>
3098         private void SetSortColumn(ComparerMode sortColumn)
3099         {
3100             if (SettingManager.Common.SortOrderLock)
3101                 return;
3102
3103             this.statuses.ToggleSortOrder(sortColumn);
3104             this.InitColumnText();
3105
3106             var list = this.CurrentListView;
3107             if (this.iconCol)
3108             {
3109                 list.Columns[0].Text = this.columnText[0];
3110                 list.Columns[1].Text = this.columnText[2];
3111             }
3112             else
3113             {
3114                 for (var i = 0; i <= 7; i++)
3115                 {
3116                     list.Columns[i].Text = this.columnText[i];
3117                 }
3118             }
3119
3120             this.PurgeListViewItemCache();
3121
3122             var tab = this.CurrentTab;
3123             var post = this.CurrentPost;
3124             if (tab.AllCount > 0 && post != null)
3125             {
3126                 var idx = tab.IndexOf(post.StatusId);
3127                 if (idx > -1)
3128                 {
3129                     this.SelectListItem(list, idx);
3130                     list.EnsureVisible(idx);
3131                 }
3132             }
3133             list.Refresh();
3134
3135             this.MarkSettingCommonModified();
3136         }
3137
3138         private void TweenMain_LocationChanged(object sender, EventArgs e)
3139         {
3140             if (this.WindowState == FormWindowState.Normal && !this.initialLayout)
3141             {
3142                 this.myLoc = this.DesktopLocation;
3143                 this.MarkSettingLocalModified();
3144             }
3145         }
3146
3147         private void ContextMenuOperate_Opening(object sender, CancelEventArgs e)
3148         {
3149             var post = this.CurrentPost;
3150             if (!this.ExistCurrentPost)
3151             {
3152                 this.ReplyStripMenuItem.Enabled = false;
3153                 this.ReplyAllStripMenuItem.Enabled = false;
3154                 this.DMStripMenuItem.Enabled = false;
3155                 this.TabMenuItem.Enabled = false;
3156                 this.IDRuleMenuItem.Enabled = false;
3157                 this.SourceRuleMenuItem.Enabled = false;
3158                 this.ReadedStripMenuItem.Enabled = false;
3159                 this.UnreadStripMenuItem.Enabled = false;
3160                 this.AuthorContextMenuItem.Visible = false;
3161                 this.RetweetedByContextMenuItem.Visible = false;
3162             }
3163             else
3164             {
3165                 this.ReplyStripMenuItem.Enabled = true;
3166                 this.ReplyAllStripMenuItem.Enabled = true;
3167                 this.DMStripMenuItem.Enabled = true;
3168                 this.TabMenuItem.Enabled = true;
3169                 this.IDRuleMenuItem.Enabled = true;
3170                 this.SourceRuleMenuItem.Enabled = true;
3171                 this.ReadedStripMenuItem.Enabled = true;
3172                 this.UnreadStripMenuItem.Enabled = true;
3173                 this.AuthorContextMenuItem.Visible = true;
3174                 this.AuthorContextMenuItem.Text = $"@{post!.ScreenName}";
3175                 this.RetweetedByContextMenuItem.Visible = post.RetweetedByUserId != null;
3176                 this.RetweetedByContextMenuItem.Text = $"@{post.RetweetedBy}";
3177             }
3178             var tab = this.CurrentTab;
3179             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm)
3180             {
3181                 this.FavAddToolStripMenuItem.Enabled = false;
3182                 this.FavRemoveToolStripMenuItem.Enabled = false;
3183                 this.StatusOpenMenuItem.Enabled = false;
3184                 this.ShowRelatedStatusesMenuItem.Enabled = false;
3185
3186                 this.ReTweetStripMenuItem.Enabled = false;
3187                 this.ReTweetUnofficialStripMenuItem.Enabled = false;
3188                 this.QuoteStripMenuItem.Enabled = false;
3189                 this.FavoriteRetweetContextMenu.Enabled = false;
3190                 this.FavoriteRetweetUnofficialContextMenu.Enabled = false;
3191             }
3192             else
3193             {
3194                 this.FavAddToolStripMenuItem.Enabled = true;
3195                 this.FavRemoveToolStripMenuItem.Enabled = true;
3196                 this.StatusOpenMenuItem.Enabled = true;
3197                 this.ShowRelatedStatusesMenuItem.Enabled = true;  // PublicSearchの時問題出るかも
3198
3199                 if (!post.CanRetweetBy(this.twitterApi.CurrentUserId))
3200                 {
3201                     this.ReTweetStripMenuItem.Enabled = false;
3202                     this.ReTweetUnofficialStripMenuItem.Enabled = false;
3203                     this.QuoteStripMenuItem.Enabled = false;
3204                     this.FavoriteRetweetContextMenu.Enabled = false;
3205                     this.FavoriteRetweetUnofficialContextMenu.Enabled = false;
3206                 }
3207                 else
3208                 {
3209                     this.ReTweetStripMenuItem.Enabled = true;
3210                     this.ReTweetUnofficialStripMenuItem.Enabled = true;
3211                     this.QuoteStripMenuItem.Enabled = true;
3212                     this.FavoriteRetweetContextMenu.Enabled = true;
3213                     this.FavoriteRetweetUnofficialContextMenu.Enabled = true;
3214                 }
3215             }
3216
3217             if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null)
3218             {
3219                 this.RepliedStatusOpenMenuItem.Enabled = false;
3220             }
3221             else
3222             {
3223                 this.RepliedStatusOpenMenuItem.Enabled = true;
3224             }
3225
3226             if (this.ExistCurrentPost && post != null)
3227             {
3228                 this.DeleteStripMenuItem.Enabled = post.CanDeleteBy(this.tw.UserId);
3229                 if (post.RetweetedByUserId == this.tw.UserId)
3230                     this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText2;
3231                 else
3232                     this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText1;
3233             }
3234         }
3235
3236         private void ReplyStripMenuItem_Click(object sender, EventArgs e)
3237             => this.MakeReplyText();
3238
3239         private void DMStripMenuItem_Click(object sender, EventArgs e)
3240             => this.MakeDirectMessageText();
3241
3242         private async Task DoStatusDelete()
3243         {
3244             var posts = this.CurrentTab.SelectedPosts;
3245             if (posts.Length == 0)
3246                 return;
3247
3248             // 選択されたツイートの中に削除可能なものが一つでもあるか
3249             if (!posts.Any(x => x.CanDeleteBy(this.tw.UserId)))
3250                 return;
3251
3252             var ret = MessageBox.Show(
3253                 this,
3254                 string.Format(Properties.Resources.DeleteStripMenuItem_ClickText1, Environment.NewLine),
3255                 Properties.Resources.DeleteStripMenuItem_ClickText2,
3256                 MessageBoxButtons.OKCancel,
3257                 MessageBoxIcon.Question);
3258
3259             if (ret != DialogResult.OK)
3260                 return;
3261
3262             var currentListView = this.CurrentListView;
3263             var focusedIndex = currentListView.FocusedItem?.Index ?? currentListView.TopItem?.Index ?? 0;
3264
3265             using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
3266             {
3267                 Exception? lastException = null;
3268                 foreach (var post in posts)
3269                 {
3270                     if (!post.CanDeleteBy(this.tw.UserId))
3271                         continue;
3272
3273                     try
3274                     {
3275                         if (post.IsDm)
3276                         {
3277                             await this.twitterApi.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture));
3278                         }
3279                         else
3280                         {
3281                             if (post.RetweetedByUserId == this.tw.UserId)
3282                             {
3283                                 // 自分が RT したツイート (自分が RT した自分のツイートも含む)
3284                                 //   => RT を取り消し
3285                                 await this.twitterApi.StatusesDestroy(post.StatusId)
3286                                     .IgnoreResponse();
3287                             }
3288                             else
3289                             {
3290                                 if (post.UserId == this.tw.UserId)
3291                                 {
3292                                     if (post.RetweetedId != null)
3293                                     {
3294                                         // 他人に RT された自分のツイート
3295                                         //   => RT 元の自分のツイートを削除
3296                                         await this.twitterApi.StatusesDestroy(post.RetweetedId.Value)
3297                                             .IgnoreResponse();
3298                                     }
3299                                     else
3300                                     {
3301                                         // 自分のツイート
3302                                         //   => ツイートを削除
3303                                         await this.twitterApi.StatusesDestroy(post.StatusId)
3304                                             .IgnoreResponse();
3305                                     }
3306                                 }
3307                             }
3308                         }
3309                     }
3310                     catch (WebApiException ex)
3311                     {
3312                         lastException = ex;
3313                         continue;
3314                     }
3315
3316                     this.statuses.RemovePostFromAllTabs(post.StatusId, setIsDeleted: true);
3317                 }
3318
3319                 if (lastException == null)
3320                     this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText4; // 成功
3321                 else
3322                     this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText3; // 失敗
3323
3324                 this.PurgeListViewItemCache();
3325
3326                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3327                 {
3328                     var tabPage = this.ListTab.TabPages[index];
3329                     var listView = (DetailsListView)tabPage.Tag;
3330
3331                     using (ControlTransaction.Update(listView))
3332                     {
3333                         listView.VirtualListSize = tab.AllCount;
3334
3335                         if (tab.TabName == this.CurrentTabName)
3336                         {
3337                             listView.SelectedIndices.Clear();
3338
3339                             if (tab.AllCount != 0)
3340                             {
3341                                 int selectedIndex;
3342                                 if (tab.AllCount - 1 > focusedIndex && focusedIndex > -1)
3343                                     selectedIndex = focusedIndex;
3344                                 else
3345                                     selectedIndex = tab.AllCount - 1;
3346
3347                                 listView.SelectedIndices.Add(selectedIndex);
3348                                 listView.EnsureVisible(selectedIndex);
3349                                 listView.FocusedItem = listView.Items[selectedIndex];
3350                             }
3351                         }
3352                     }
3353
3354                     if (SettingManager.Common.TabIconDisp && tab.UnreadCount == 0)
3355                     {
3356                         if (tabPage.ImageIndex == 0)
3357                             tabPage.ImageIndex = -1; // タブアイコン
3358                     }
3359                 }
3360
3361                 if (!SettingManager.Common.TabIconDisp)
3362                     this.ListTab.Refresh();
3363             }
3364         }
3365
3366         private async void DeleteStripMenuItem_Click(object sender, EventArgs e)
3367             => await this.DoStatusDelete();
3368
3369         private void ReadedStripMenuItem_Click(object sender, EventArgs e)
3370         {
3371             using (ControlTransaction.Update(this.CurrentListView))
3372             {
3373                 var tab = this.CurrentTab;
3374                 foreach (var statusId in tab.SelectedStatusIds)
3375                 {
3376                     this.statuses.SetReadAllTab(statusId, read: true);
3377                     var idx = tab.IndexOf(statusId);
3378                     this.ChangeCacheStyleRead(true, idx);
3379                 }
3380                 this.ColorizeList();
3381             }
3382             if (SettingManager.Common.TabIconDisp)
3383             {
3384                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3385                 {
3386                     if (tab.UnreadCount == 0)
3387                     {
3388                         var tabPage = this.ListTab.TabPages[index];
3389                         if (tabPage.ImageIndex == 0)
3390                             tabPage.ImageIndex = -1; // タブアイコン
3391                     }
3392                 }
3393             }
3394             if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
3395         }
3396
3397         private void UnreadStripMenuItem_Click(object sender, EventArgs e)
3398         {
3399             using (ControlTransaction.Update(this.CurrentListView))
3400             {
3401                 var tab = this.CurrentTab;
3402                 foreach (var statusId in tab.SelectedStatusIds)
3403                 {
3404                     this.statuses.SetReadAllTab(statusId, read: false);
3405                     var idx = tab.IndexOf(statusId);
3406                     this.ChangeCacheStyleRead(false, idx);
3407                 }
3408                 this.ColorizeList();
3409             }
3410             if (SettingManager.Common.TabIconDisp)
3411             {
3412                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3413                 {
3414                     if (tab.UnreadCount > 0)
3415                     {
3416                         var tabPage = this.ListTab.TabPages[index];
3417                         if (tabPage.ImageIndex == -1)
3418                             tabPage.ImageIndex = 0; // タブアイコン
3419                     }
3420                 }
3421             }
3422             if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
3423         }
3424
3425         private async void RefreshStripMenuItem_Click(object sender, EventArgs e)
3426             => await this.DoRefresh();
3427
3428         private async Task DoRefresh()
3429             => await this.RefreshTabAsync(this.CurrentTab);
3430
3431         private async Task DoRefreshMore()
3432             => await this.RefreshTabAsync(this.CurrentTab, backward: true);
3433
3434         private DialogResult ShowSettingDialog(bool showTaskbarIcon = false)
3435         {
3436             var result = DialogResult.Abort;
3437
3438             using var settingDialog = new AppendSettingDialog();
3439             settingDialog.Icon = this.mainIcon;
3440             settingDialog.Owner = this;
3441             settingDialog.ShowInTaskbar = showTaskbarIcon;
3442             settingDialog.IntervalChanged += this.TimerInterval_Changed;
3443
3444             settingDialog.Tw = this.tw;
3445             settingDialog.TwitterApi = this.twitterApi;
3446
3447             settingDialog.LoadConfig(SettingManager.Common, SettingManager.Local);
3448
3449             try
3450             {
3451                 result = settingDialog.ShowDialog(this);
3452             }
3453             catch (Exception)
3454             {
3455                 return DialogResult.Abort;
3456             }
3457
3458             if (result == DialogResult.OK)
3459             {
3460                 lock (this.syncObject)
3461                 {
3462                     settingDialog.SaveConfig(SettingManager.Common, SettingManager.Local);
3463                 }
3464             }
3465
3466             return result;
3467         }
3468
3469         private async void SettingStripMenuItem_Click(object sender, EventArgs e)
3470         {
3471             // 設定画面表示前のユーザー情報
3472             var oldUser = new { this.tw.AccessToken, this.tw.AccessTokenSecret, this.tw.Username, this.tw.UserId };
3473
3474             var oldIconSz = SettingManager.Common.IconSize;
3475
3476             if (this.ShowSettingDialog() == DialogResult.OK)
3477             {
3478                 lock (this.syncObject)
3479                 {
3480                     this.tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck;
3481                     this.tw.ReadOwnPost = SettingManager.Common.ReadOwnPost;
3482                     ShortUrl.Instance.DisableExpanding = !SettingManager.Common.TinyUrlResolve;
3483                     ShortUrl.Instance.BitlyAccessToken = SettingManager.Common.BitlyAccessToken;
3484                     ShortUrl.Instance.BitlyId = SettingManager.Common.BilyUser;
3485                     ShortUrl.Instance.BitlyKey = SettingManager.Common.BitlyPwd;
3486                     TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost;
3487
3488                     Networking.DefaultTimeout = TimeSpan.FromSeconds(SettingManager.Common.DefaultTimeOut);
3489                     Networking.UploadImageTimeout = TimeSpan.FromSeconds(SettingManager.Common.UploadImageTimeout);
3490                     Networking.SetWebProxy(
3491                         SettingManager.Local.ProxyType,
3492                         SettingManager.Local.ProxyAddress,
3493                         SettingManager.Local.ProxyPort,
3494                         SettingManager.Local.ProxyUser,
3495                         SettingManager.Local.ProxyPassword);
3496                     Networking.ForceIPv4 = SettingManager.Common.ForceIPv4;
3497
3498                     this.ImageSelector.Reset(this.tw, this.tw.Configuration);
3499
3500                     try
3501                     {
3502                         if (SettingManager.Common.TabIconDisp)
3503                         {
3504                             this.ListTab.DrawItem -= this.ListTab_DrawItem;
3505                             this.ListTab.DrawMode = TabDrawMode.Normal;
3506                             this.ListTab.ImageList = this.TabImage;
3507                         }
3508                         else
3509                         {
3510                             this.ListTab.DrawItem -= this.ListTab_DrawItem;
3511                             this.ListTab.DrawItem += this.ListTab_DrawItem;
3512                             this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed;
3513                             this.ListTab.ImageList = null;
3514                         }
3515                     }
3516                     catch (Exception ex)
3517                     {
3518                         ex.Data["Instance"] = "ListTab(TabIconDisp)";
3519                         ex.Data["IsTerminatePermission"] = false;
3520                         throw;
3521                     }
3522
3523                     try
3524                     {
3525                         if (!SettingManager.Common.UnreadManage)
3526                         {
3527                             this.ReadedStripMenuItem.Enabled = false;
3528                             this.UnreadStripMenuItem.Enabled = false;
3529                             if (SettingManager.Common.TabIconDisp)
3530                             {
3531                                 foreach (TabPage myTab in this.ListTab.TabPages)
3532                                 {
3533                                     myTab.ImageIndex = -1;
3534                                 }
3535                             }
3536                         }
3537                         else
3538                         {
3539                             this.ReadedStripMenuItem.Enabled = true;
3540                             this.UnreadStripMenuItem.Enabled = true;
3541                         }
3542                     }
3543                     catch (Exception ex)
3544                     {
3545                         ex.Data["Instance"] = "ListTab(UnreadManage)";
3546                         ex.Data["IsTerminatePermission"] = false;
3547                         throw;
3548                     }
3549
3550                     // タブの表示位置の決定
3551                     this.SetTabAlignment();
3552
3553                     this.SplitContainer1.IsPanelInverted = !SettingManager.Common.StatusAreaAtBottom;
3554
3555                     var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
3556                     imgazyobizinet.Enabled = SettingManager.Common.EnableImgAzyobuziNet;
3557                     imgazyobizinet.DisabledInDM = SettingManager.Common.ImgAzyobuziNetDisabledInDM;
3558
3559                     this.NewPostPopMenuItem.Checked = SettingManager.Common.NewAllPop;
3560                     this.NotifyFileMenuItem.Checked = SettingManager.Common.NewAllPop;
3561                     this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound;
3562                     this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound;
3563                     this.fntUnread = SettingManager.Local.FontUnread;
3564                     this.clUnread = SettingManager.Local.ColorUnread;
3565                     this.fntReaded = SettingManager.Local.FontRead;
3566                     this.clReaded = SettingManager.Local.ColorRead;
3567                     this.clFav = SettingManager.Local.ColorFav;
3568                     this.clOWL = SettingManager.Local.ColorOWL;
3569                     this.clRetweet = SettingManager.Local.ColorRetweet;
3570                     this.fntDetail = SettingManager.Local.FontDetail;
3571                     this.clDetail = SettingManager.Local.ColorDetail;
3572                     this.clDetailLink = SettingManager.Local.ColorDetailLink;
3573                     this.clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor;
3574                     this.clSelf = SettingManager.Local.ColorSelf;
3575                     this.clAtSelf = SettingManager.Local.ColorAtSelf;
3576                     this.clTarget = SettingManager.Local.ColorTarget;
3577                     this.clAtTarget = SettingManager.Local.ColorAtTarget;
3578                     this.clAtFromTarget = SettingManager.Local.ColorAtFromTarget;
3579                     this.clAtTo = SettingManager.Local.ColorAtTo;
3580                     this.clListBackcolor = SettingManager.Local.ColorListBackcolor;
3581                     this.clInputBackcolor = SettingManager.Local.ColorInputBackcolor;
3582                     this.clInputFont = SettingManager.Local.ColorInputFont;
3583                     this.fntInputFont = SettingManager.Local.FontInputFont;
3584                     this.brsBackColorMine.Dispose();
3585                     this.brsBackColorAt.Dispose();
3586                     this.brsBackColorYou.Dispose();
3587                     this.brsBackColorAtYou.Dispose();
3588                     this.brsBackColorAtFromTarget.Dispose();
3589                     this.brsBackColorAtTo.Dispose();
3590                     this.brsBackColorNone.Dispose();
3591                     this.brsBackColorMine = new SolidBrush(this.clSelf);
3592                     this.brsBackColorAt = new SolidBrush(this.clAtSelf);
3593                     this.brsBackColorYou = new SolidBrush(this.clTarget);
3594                     this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
3595                     this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
3596                     this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
3597                     this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
3598
3599                     try
3600                     {
3601                         if (this.StatusText.Focused) this.StatusText.BackColor = this.clInputBackcolor;
3602                         this.StatusText.Font = this.fntInputFont;
3603                         this.StatusText.ForeColor = this.clInputFont;
3604                     }
3605                     catch (Exception ex)
3606                     {
3607                         MessageBox.Show(ex.Message);
3608                     }
3609
3610                     try
3611                     {
3612                         this.InitDetailHtmlFormat();
3613                     }
3614                     catch (Exception ex)
3615                     {
3616                         ex.Data["Instance"] = "Font";
3617                         ex.Data["IsTerminatePermission"] = false;
3618                         throw;
3619                     }
3620
3621                     try
3622                     {
3623                         if (SettingManager.Common.TabIconDisp)
3624                         {
3625                             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3626                             {
3627                                 var tabPage = this.ListTab.TabPages[index];
3628                                 if (tab.UnreadCount == 0)
3629                                     tabPage.ImageIndex = -1;
3630                                 else
3631                                     tabPage.ImageIndex = 0;
3632                             }
3633                         }
3634                     }
3635                     catch (Exception ex)
3636                     {
3637                         ex.Data["Instance"] = "ListTab(TabIconDisp no2)";
3638                         ex.Data["IsTerminatePermission"] = false;
3639                         throw;
3640                     }
3641
3642                     try
3643                     {
3644                         var oldIconCol = this.iconCol;
3645
3646                         if (SettingManager.Common.IconSize != oldIconSz)
3647                             this.ApplyListViewIconSize(SettingManager.Common.IconSize);
3648
3649                         foreach (TabPage tp in this.ListTab.TabPages)
3650                         {
3651                             var lst = (DetailsListView)tp.Tag;
3652
3653                             using (ControlTransaction.Update(lst))
3654                             {
3655                                 lst.GridLines = SettingManager.Common.ShowGrid;
3656                                 lst.Font = this.fntReaded;
3657                                 lst.BackColor = this.clListBackcolor;
3658
3659                                 if (this.iconCol != oldIconCol)
3660                                     this.ResetColumns(lst);
3661                             }
3662                         }
3663                     }
3664                     catch (Exception ex)
3665                     {
3666                         ex.Data["Instance"] = "ListView(IconSize)";
3667                         ex.Data["IsTerminatePermission"] = false;
3668                         throw;
3669                     }
3670
3671                     this.SetMainWindowTitle();
3672                     this.SetNotifyIconText();
3673
3674                     this.PurgeListViewItemCache();
3675                     this.CurrentListView.Refresh();
3676                     this.ListTab.Refresh();
3677
3678                     this.hookGlobalHotkey.UnregisterAllOriginalHotkey();
3679                     if (SettingManager.Common.HotkeyEnabled)
3680                     {
3681                         // グローバルホットキーの登録。設定で変更可能にするかも
3682                         var modKey = HookGlobalHotkey.ModKeys.None;
3683                         if ((SettingManager.Common.HotkeyModifier & Keys.Alt) == Keys.Alt)
3684                             modKey |= HookGlobalHotkey.ModKeys.Alt;
3685                         if ((SettingManager.Common.HotkeyModifier & Keys.Control) == Keys.Control)
3686                             modKey |= HookGlobalHotkey.ModKeys.Ctrl;
3687                         if ((SettingManager.Common.HotkeyModifier & Keys.Shift) == Keys.Shift)
3688                             modKey |= HookGlobalHotkey.ModKeys.Shift;
3689                         if ((SettingManager.Common.HotkeyModifier & Keys.LWin) == Keys.LWin)
3690                             modKey |= HookGlobalHotkey.ModKeys.Win;
3691
3692                         this.hookGlobalHotkey.RegisterOriginalHotkey(SettingManager.Common.HotkeyKey, SettingManager.Common.HotkeyValue, modKey);
3693                     }
3694
3695                     if (SettingManager.Common.IsUseNotifyGrowl) this.gh.RegisterGrowl();
3696                     try
3697                     {
3698                         this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
3699                     }
3700                     catch (Exception)
3701                     {
3702                     }
3703                 }
3704             }
3705             else
3706             {
3707                 // キャンセル時は Twitter クラスの認証情報を画面表示前の状態に戻す
3708                 this.tw.Initialize(oldUser.AccessToken, oldUser.AccessTokenSecret, oldUser.Username, oldUser.UserId);
3709             }
3710
3711             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3712
3713             this.TopMost = SettingManager.Common.AlwaysTop;
3714             this.SaveConfigsAll(false);
3715
3716             if (this.tw.UserId != oldUser.UserId)
3717                 await this.DoGetFollowersMenu();
3718         }
3719
3720         /// <summary>
3721         /// タブの表示位置を設定する
3722         /// </summary>
3723         private void SetTabAlignment()
3724         {
3725             var newAlignment = SettingManager.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top;
3726             if (this.ListTab.Alignment == newAlignment) return;
3727
3728             // 各タブのリスト上の選択位置などを退避
3729             var listSelections = this.SaveListViewSelection();
3730
3731             this.ListTab.Alignment = newAlignment;
3732
3733             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3734             {
3735                 var lst = (DetailsListView)this.ListTab.TabPages[index].Tag;
3736                 using (ControlTransaction.Update(lst))
3737                 {
3738                     // 選択位置などを復元
3739                     this.RestoreListViewSelection(lst, tab, listSelections[tab.TabName]);
3740                 }
3741             }
3742         }
3743
3744         private void ApplyListViewIconSize(MyCommon.IconSizes iconSz)
3745         {
3746             // アイコンサイズの再設定
3747             this.iconSz = iconSz switch
3748             {
3749                 MyCommon.IconSizes.IconNone => 0,
3750                 MyCommon.IconSizes.Icon16 => 16,
3751                 MyCommon.IconSizes.Icon24 => 26,
3752                 MyCommon.IconSizes.Icon48 => 48,
3753                 MyCommon.IconSizes.Icon48_2 => 48,
3754                 _ => throw new InvalidEnumArgumentException(nameof(iconSz), (int)iconSz, typeof(MyCommon.IconSizes)),
3755             };
3756             this.iconCol = iconSz == MyCommon.IconSizes.Icon48_2;
3757
3758             this.PurgeListViewItemCache();
3759
3760             if (this.iconSz > 0)
3761             {
3762                 // ディスプレイの DPI 設定を考慮したサイズを設定する
3763                 this.listViewImageList.ImageSize = new Size(
3764                     1,
3765                     (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Height));
3766             }
3767             else
3768             {
3769                 this.listViewImageList.ImageSize = new Size(1, 1);
3770             }
3771         }
3772
3773         private void ResetColumns(DetailsListView list)
3774         {
3775             using (ControlTransaction.Update(list))
3776             using (ControlTransaction.Layout(list, false))
3777             {
3778                 // カラムヘッダの再設定
3779                 list.ColumnClick -= this.MyList_ColumnClick;
3780                 list.DrawColumnHeader -= this.MyList_DrawColumnHeader;
3781                 list.ColumnReordered -= this.MyList_ColumnReordered;
3782                 list.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
3783
3784                 var cols = list.Columns.Cast<ColumnHeader>().ToList();
3785                 list.Columns.Clear();
3786                 cols.ForEach(col => col.Dispose());
3787                 cols.Clear();
3788
3789                 this.InitColumns(list, true);
3790
3791                 list.ColumnClick += this.MyList_ColumnClick;
3792                 list.DrawColumnHeader += this.MyList_DrawColumnHeader;
3793                 list.ColumnReordered += this.MyList_ColumnReordered;
3794                 list.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
3795             }
3796         }
3797
3798         public void AddNewTabForSearch(string searchWord)
3799         {
3800             // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了
3801             foreach (var tb in this.statuses.GetTabsByType<PublicSearchTabModel>())
3802             {
3803                 if (tb.SearchWords == searchWord && MyCommon.IsNullOrEmpty(tb.SearchLang))
3804                 {
3805                     var tabIndex = this.statuses.Tabs.IndexOf(tb);
3806                     this.ListTab.SelectedIndex = tabIndex;
3807                     return;
3808                 }
3809             }
3810             // ユニークなタブ名生成
3811             var tabName = searchWord;
3812             for (var i = 0; i <= 100; i++)
3813             {
3814                 if (this.statuses.ContainsTab(tabName))
3815                     tabName += "_";
3816                 else
3817                     break;
3818             }
3819             // タブ追加
3820             var tab = new PublicSearchTabModel(tabName);
3821             this.statuses.AddTab(tab);
3822             this.AddNewTab(tab, startup: false);
3823             // 追加したタブをアクティブに
3824             this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1;
3825             // 検索条件の設定
3826             var tabPage = this.CurrentTabPage;
3827             var cmb = (ComboBox)tabPage.Controls["panelSearch"].Controls["comboSearch"];
3828             cmb.Items.Add(searchWord);
3829             cmb.Text = searchWord;
3830             this.SaveConfigsTabs();
3831             // 検索実行
3832             this.SearchButton_Click(tabPage.Controls["panelSearch"].Controls["comboSearch"], EventArgs.Empty);
3833         }
3834
3835         private async Task ShowUserTimeline()
3836         {
3837             var post = this.CurrentPost;
3838             if (post == null || !this.ExistCurrentPost) return;
3839             await this.AddNewTabForUserTimeline(post.ScreenName);
3840         }
3841
3842         private async Task ShowRetweeterTimeline()
3843         {
3844             var retweetedBy = this.CurrentPost?.RetweetedBy;
3845             if (retweetedBy == null || !this.ExistCurrentPost) return;
3846             await this.AddNewTabForUserTimeline(retweetedBy);
3847         }
3848
3849         private void SearchComboBox_KeyDown(object sender, KeyEventArgs e)
3850         {
3851             if (e.KeyCode == Keys.Escape)
3852             {
3853                 this.RemoveSpecifiedTab(this.CurrentTabName, false);
3854                 this.SaveConfigsTabs();
3855                 e.SuppressKeyPress = true;
3856             }
3857         }
3858
3859         public async Task AddNewTabForUserTimeline(string user)
3860         {
3861             // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了
3862             foreach (var tb in this.statuses.GetTabsByType<UserTimelineTabModel>())
3863             {
3864                 if (tb.ScreenName == user)
3865                 {
3866                     var tabIndex = this.statuses.Tabs.IndexOf(tb);
3867                     this.ListTab.SelectedIndex = tabIndex;
3868                     return;
3869                 }
3870             }
3871             // ユニークなタブ名生成
3872             var tabName = "user:" + user;
3873             while (this.statuses.ContainsTab(tabName))
3874             {
3875                 tabName += "_";
3876             }
3877             // タブ追加
3878             var tab = new UserTimelineTabModel(tabName, user);
3879             this.statuses.AddTab(tab);
3880             this.AddNewTab(tab, startup: false);
3881             // 追加したタブをアクティブに
3882             this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1;
3883             this.SaveConfigsTabs();
3884             // 検索実行
3885             await this.RefreshTabAsync(tab);
3886         }
3887
3888         public bool AddNewTab(TabModel tab, bool startup)
3889         {
3890             // 重複チェック
3891             if (this.ListTab.TabPages.Cast<TabPage>().Any(x => x.Text == tab.TabName))
3892                 return false;
3893
3894             // 新規タブ名チェック
3895             if (tab.TabName == Properties.Resources.AddNewTabText1) return false;
3896
3897             var tabPage = new TabPage();
3898             var listCustom = new DetailsListView();
3899
3900             var cnt = this.statuses.Tabs.Count;
3901
3902             // ToDo:Create and set controls follow tabtypes
3903
3904             using (ControlTransaction.Update(listCustom))
3905             using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false))
3906             using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false))
3907             using (ControlTransaction.Layout(this.SplitContainer1, false))
3908             using (ControlTransaction.Layout(this.ListTab, false))
3909             using (ControlTransaction.Layout(this))
3910             using (ControlTransaction.Layout(tabPage, false))
3911             {
3912                 tabPage.Controls.Add(listCustom);
3913
3914                 // UserTimeline関連
3915                 var userTab = tab as UserTimelineTabModel;
3916                 var listTab = tab as ListTimelineTabModel;
3917                 var searchTab = tab as PublicSearchTabModel;
3918
3919                 if (userTab != null || listTab != null)
3920                 {
3921                     var label = new Label
3922                     {
3923                         Dock = DockStyle.Top,
3924                         Name = "labelUser",
3925                         TabIndex = 0,
3926                     };
3927
3928                     if (listTab != null)
3929                     {
3930                         label.Text = listTab.ListInfo.ToString();
3931                     }
3932                     else if (userTab != null)
3933                     {
3934                         label.Text = userTab.ScreenName + "'s Timeline";
3935                     }
3936                     label.TextAlign = ContentAlignment.MiddleLeft;
3937                     using (var tmpComboBox = new ComboBox())
3938                     {
3939                         label.Height = tmpComboBox.Height;
3940                     }
3941                     tabPage.Controls.Add(label);
3942                 }
3943                 // 検索関連の準備
3944                 else if (searchTab != null)
3945                 {
3946                     var pnl = new Panel();
3947
3948                     var lbl = new Label();
3949                     var cmb = new ComboBox();
3950                     var btn = new Button();
3951                     var cmbLang = new ComboBox();
3952
3953                     using (ControlTransaction.Layout(pnl, false))
3954                     {
3955                         pnl.Controls.Add(cmb);
3956                         pnl.Controls.Add(cmbLang);
3957                         pnl.Controls.Add(btn);
3958                         pnl.Controls.Add(lbl);
3959                         pnl.Name = "panelSearch";
3960                         pnl.TabIndex = 0;
3961                         pnl.Dock = DockStyle.Top;
3962                         pnl.Height = cmb.Height;
3963                         pnl.Enter += this.SearchControls_Enter;
3964                         pnl.Leave += this.SearchControls_Leave;
3965
3966                         cmb.Text = "";
3967                         cmb.Anchor = AnchorStyles.Left | AnchorStyles.Right;
3968                         cmb.Dock = DockStyle.Fill;
3969                         cmb.Name = "comboSearch";
3970                         cmb.DropDownStyle = ComboBoxStyle.DropDown;
3971                         cmb.ImeMode = ImeMode.NoControl;
3972                         cmb.TabStop = false;
3973                         cmb.TabIndex = 1;
3974                         cmb.AutoCompleteMode = AutoCompleteMode.None;
3975                         cmb.KeyDown += this.SearchComboBox_KeyDown;
3976
3977                         cmbLang.Text = "";
3978                         cmbLang.Anchor = AnchorStyles.Left | AnchorStyles.Right;
3979                         cmbLang.Dock = DockStyle.Right;
3980                         cmbLang.Width = 50;
3981                         cmbLang.Name = "comboLang";
3982                         cmbLang.DropDownStyle = ComboBoxStyle.DropDownList;
3983                         cmbLang.TabStop = false;
3984                         cmbLang.TabIndex = 2;
3985                         cmbLang.Items.Add("");
3986                         cmbLang.Items.Add("ja");
3987                         cmbLang.Items.Add("en");
3988                         cmbLang.Items.Add("ar");
3989                         cmbLang.Items.Add("da");
3990                         cmbLang.Items.Add("nl");
3991                         cmbLang.Items.Add("fa");
3992                         cmbLang.Items.Add("fi");
3993                         cmbLang.Items.Add("fr");
3994                         cmbLang.Items.Add("de");
3995                         cmbLang.Items.Add("hu");
3996                         cmbLang.Items.Add("is");
3997                         cmbLang.Items.Add("it");
3998                         cmbLang.Items.Add("no");
3999                         cmbLang.Items.Add("pl");
4000                         cmbLang.Items.Add("pt");
4001                         cmbLang.Items.Add("ru");
4002                         cmbLang.Items.Add("es");
4003                         cmbLang.Items.Add("sv");
4004                         cmbLang.Items.Add("th");
4005
4006                         lbl.Text = "Search(C-S-f)";
4007                         lbl.Name = "label1";
4008                         lbl.Dock = DockStyle.Left;
4009                         lbl.Width = 90;
4010                         lbl.Height = cmb.Height;
4011                         lbl.TextAlign = ContentAlignment.MiddleLeft;
4012                         lbl.TabIndex = 0;
4013
4014                         btn.Text = "Search";
4015                         btn.Name = "buttonSearch";
4016                         btn.UseVisualStyleBackColor = true;
4017                         btn.Dock = DockStyle.Right;
4018                         btn.TabStop = false;
4019                         btn.TabIndex = 3;
4020                         btn.Click += this.SearchButton_Click;
4021
4022                         if (!MyCommon.IsNullOrEmpty(searchTab.SearchWords))
4023                         {
4024                             cmb.Items.Add(searchTab.SearchWords);
4025                             cmb.Text = searchTab.SearchWords;
4026                         }
4027
4028                         cmbLang.Text = searchTab.SearchLang;
4029
4030                         tabPage.Controls.Add(pnl);
4031                     }
4032                 }
4033
4034                 tabPage.Tag = listCustom;
4035                 this.ListTab.Controls.Add(tabPage);
4036
4037                 tabPage.Location = new Point(4, 4);
4038                 tabPage.Name = "CTab" + cnt;
4039                 tabPage.Size = new Size(380, 260);
4040                 tabPage.TabIndex = 2 + cnt;
4041                 tabPage.Text = tab.TabName;
4042                 tabPage.UseVisualStyleBackColor = true;
4043                 tabPage.AccessibleRole = AccessibleRole.PageTab;
4044
4045                 listCustom.AccessibleName = Properties.Resources.AddNewTab_ListView_AccessibleName;
4046                 listCustom.TabIndex = 1;
4047                 listCustom.AllowColumnReorder = true;
4048                 listCustom.ContextMenuStrip = this.ContextMenuOperate;
4049                 listCustom.ColumnHeaderContextMenuStrip = this.ContextMenuColumnHeader;
4050                 listCustom.Dock = DockStyle.Fill;
4051                 listCustom.FullRowSelect = true;
4052                 listCustom.HideSelection = false;
4053                 listCustom.Location = new Point(0, 0);
4054                 listCustom.Margin = new Padding(0);
4055                 listCustom.Name = "CList" + Environment.TickCount;
4056                 listCustom.ShowItemToolTips = true;
4057                 listCustom.Size = new Size(380, 260);
4058                 listCustom.UseCompatibleStateImageBehavior = false;
4059                 listCustom.View = View.Details;
4060                 listCustom.OwnerDraw = true;
4061                 listCustom.VirtualMode = true;
4062                 listCustom.Font = this.fntReaded;
4063                 listCustom.BackColor = this.clListBackcolor;
4064
4065                 listCustom.GridLines = SettingManager.Common.ShowGrid;
4066                 listCustom.AllowDrop = true;
4067
4068                 listCustom.SmallImageList = this.listViewImageList;
4069
4070                 this.InitColumns(listCustom, startup);
4071
4072                 listCustom.SelectedIndexChanged += this.MyList_SelectedIndexChanged;
4073                 listCustom.MouseDoubleClick += this.MyList_MouseDoubleClick;
4074                 listCustom.ColumnClick += this.MyList_ColumnClick;
4075                 listCustom.DrawColumnHeader += this.MyList_DrawColumnHeader;
4076                 listCustom.DragDrop += this.TweenMain_DragDrop;
4077                 listCustom.DragEnter += this.TweenMain_DragEnter;
4078                 listCustom.DragOver += this.TweenMain_DragOver;
4079                 listCustom.DrawItem += this.MyList_DrawItem;
4080                 listCustom.MouseClick += this.MyList_MouseClick;
4081                 listCustom.ColumnReordered += this.MyList_ColumnReordered;
4082                 listCustom.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
4083                 listCustom.CacheVirtualItems += this.MyList_CacheVirtualItems;
4084                 listCustom.RetrieveVirtualItem += this.MyList_RetrieveVirtualItem;
4085                 listCustom.DrawSubItem += this.MyList_DrawSubItem;
4086                 listCustom.HScrolled += this.MyList_HScrolled;
4087             }
4088
4089             return true;
4090         }
4091
4092         public bool RemoveSpecifiedTab(string tabName, bool confirm)
4093         {
4094             var tabInfo = this.statuses.GetTabByName(tabName);
4095             if (tabInfo == null || tabInfo.IsDefaultTabType || tabInfo.Protected)
4096                 return false;
4097
4098             if (confirm)
4099             {
4100                 var tmp = string.Format(Properties.Resources.RemoveSpecifiedTabText1, Environment.NewLine);
4101                 var result = MessageBox.Show(
4102                     tmp,
4103                     tabName + " " + Properties.Resources.RemoveSpecifiedTabText2,
4104                     MessageBoxButtons.OKCancel,
4105                     MessageBoxIcon.Question,
4106                     MessageBoxDefaultButton.Button2);
4107                 if (result == DialogResult.Cancel)
4108                 {
4109                     return false;
4110                 }
4111             }
4112
4113             var tabIndex = this.statuses.Tabs.IndexOf(tabName);
4114             if (tabIndex == -1)
4115                 return false;
4116
4117             var tabPage = this.ListTab.TabPages[tabIndex];
4118
4119             this.SetListProperty();   // 他のタブに列幅等を反映
4120
4121             // オブジェクトインスタンスの削除
4122             var listCustom = (DetailsListView)tabPage.Tag;
4123             tabPage.Tag = null;
4124
4125             using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false))
4126             using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false))
4127             using (ControlTransaction.Layout(this.SplitContainer1, false))
4128             using (ControlTransaction.Layout(this.ListTab, false))
4129             using (ControlTransaction.Layout(this))
4130             using (ControlTransaction.Layout(tabPage, false))
4131             {
4132                 if (this.CurrentTabName == tabName)
4133                 {
4134                     this.ListTab.SelectTab((this.beforeSelectedTab != null && this.ListTab.TabPages.Contains(this.beforeSelectedTab)) ? this.beforeSelectedTab : this.ListTab.TabPages[0]);
4135                     this.beforeSelectedTab = null;
4136                 }
4137                 this.ListTab.Controls.Remove(tabPage);
4138
4139                 // 後付けのコントロールを破棄
4140                 if (tabInfo.TabType == MyCommon.TabUsageType.UserTimeline || tabInfo.TabType == MyCommon.TabUsageType.Lists)
4141                 {
4142                     using var label = tabPage.Controls["labelUser"];
4143                     tabPage.Controls.Remove(label);
4144                 }
4145                 else if (tabInfo.TabType == MyCommon.TabUsageType.PublicSearch)
4146                 {
4147                     using var pnl = tabPage.Controls["panelSearch"];
4148
4149                     pnl.Enter -= this.SearchControls_Enter;
4150                     pnl.Leave -= this.SearchControls_Leave;
4151                     tabPage.Controls.Remove(pnl);
4152
4153                     foreach (Control ctrl in pnl.Controls)
4154                     {
4155                         if (ctrl.Name == "buttonSearch")
4156                         {
4157                             ctrl.Click -= this.SearchButton_Click;
4158                         }
4159                         else if (ctrl.Name == "comboSearch")
4160                         {
4161                             ctrl.KeyDown -= this.SearchComboBox_KeyDown;
4162                         }
4163                         pnl.Controls.Remove(ctrl);
4164                         ctrl.Dispose();
4165                     }
4166                 }
4167
4168                 tabPage.Controls.Remove(listCustom);
4169
4170                 listCustom.SelectedIndexChanged -= this.MyList_SelectedIndexChanged;
4171                 listCustom.MouseDoubleClick -= this.MyList_MouseDoubleClick;
4172                 listCustom.ColumnClick -= this.MyList_ColumnClick;
4173                 listCustom.DrawColumnHeader -= this.MyList_DrawColumnHeader;
4174                 listCustom.DragDrop -= this.TweenMain_DragDrop;
4175                 listCustom.DragEnter -= this.TweenMain_DragEnter;
4176                 listCustom.DragOver -= this.TweenMain_DragOver;
4177                 listCustom.DrawItem -= this.MyList_DrawItem;
4178                 listCustom.MouseClick -= this.MyList_MouseClick;
4179                 listCustom.ColumnReordered -= this.MyList_ColumnReordered;
4180                 listCustom.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
4181                 listCustom.CacheVirtualItems -= this.MyList_CacheVirtualItems;
4182                 listCustom.RetrieveVirtualItem -= this.MyList_RetrieveVirtualItem;
4183                 listCustom.DrawSubItem -= this.MyList_DrawSubItem;
4184                 listCustom.HScrolled -= this.MyList_HScrolled;
4185
4186                 var cols = listCustom.Columns.Cast<ColumnHeader>().ToList<ColumnHeader>();
4187                 listCustom.Columns.Clear();
4188                 cols.ForEach(col => col.Dispose());
4189                 cols.Clear();
4190
4191                 listCustom.ContextMenuStrip = null;
4192                 listCustom.ColumnHeaderContextMenuStrip = null;
4193                 listCustom.Font = null;
4194
4195                 listCustom.SmallImageList = null;
4196                 listCustom.ListViewItemSorter = null;
4197
4198                 // キャッシュのクリア
4199                 this.PurgeListViewItemCache();
4200             }
4201
4202             tabPage.Dispose();
4203             listCustom.Dispose();
4204             this.statuses.RemoveTab(tabName);
4205
4206             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
4207             {
4208                 var lst = (DetailsListView)this.ListTab.TabPages[index].Tag;
4209                 lst.VirtualListSize = tab.AllCount;
4210             }
4211
4212             return true;
4213         }
4214
4215         private void ListTab_Deselected(object sender, TabControlEventArgs e)
4216         {
4217             this.PurgeListViewItemCache();
4218             this.beforeSelectedTab = e.TabPage;
4219         }
4220
4221         private void ListTab_MouseMove(object sender, MouseEventArgs e)
4222         {
4223             // タブのD&D
4224
4225             if (!SettingManager.Common.TabMouseLock && e.Button == MouseButtons.Left && this.tabDrag)
4226             {
4227                 var tn = "";
4228                 var dragEnableRectangle = new Rectangle(this.tabMouseDownPoint.X - (SystemInformation.DragSize.Width / 2), this.tabMouseDownPoint.Y - (SystemInformation.DragSize.Height / 2), SystemInformation.DragSize.Width, SystemInformation.DragSize.Height);
4229                 if (!dragEnableRectangle.Contains(e.Location))
4230                 {
4231                     // タブが多段の場合にはMouseDownの前の段階で選択されたタブの段が変わっているので、このタイミングでカーソルの位置からタブを判定出来ない。
4232                     tn = this.CurrentTabName;
4233                 }
4234
4235                 if (MyCommon.IsNullOrEmpty(tn)) return;
4236
4237                 var tabIndex = this.statuses.Tabs.IndexOf(tn);
4238                 if (tabIndex != -1)
4239                 {
4240                     var tabPage = this.ListTab.TabPages[tabIndex];
4241                     this.ListTab.DoDragDrop(tabPage, DragDropEffects.All);
4242                 }
4243             }
4244             else
4245             {
4246                 this.tabDrag = false;
4247             }
4248
4249             var cpos = new Point(e.X, e.Y);
4250             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
4251             {
4252                 var rect = this.ListTab.GetTabRect(index);
4253                 if (rect.Contains(cpos))
4254                 {
4255                     this.rclickTabName = tab.TabName;
4256                     break;
4257                 }
4258             }
4259         }
4260
4261         private void ListTab_SelectedIndexChanged(object sender, EventArgs e)
4262         {
4263             this.SetMainWindowTitle();
4264             this.SetStatusLabelUrl();
4265             this.SetApiStatusLabel();
4266             if (this.ListTab.Focused || ((Control)this.CurrentTabPage.Tag).Focused)
4267                 this.Tag = this.ListTab.Tag;
4268             this.TabMenuControl(this.CurrentTabName);
4269             this.PushSelectPostChain();
4270             this.DispSelectedPost();
4271         }
4272
4273         private void SetListProperty()
4274         {
4275             if (!this.isColumnChanged) return;
4276
4277             var currentListView = this.CurrentListView;
4278
4279             var dispOrder = new int[currentListView.Columns.Count];
4280             for (var i = 0; i < currentListView.Columns.Count; i++)
4281             {
4282                 for (var j = 0; j < currentListView.Columns.Count; j++)
4283                 {
4284                     if (currentListView.Columns[j].DisplayIndex == i)
4285                     {
4286                         dispOrder[i] = j;
4287                         break;
4288                     }
4289                 }
4290             }
4291
4292             // 列幅、列並びを他のタブに設定
4293             foreach (TabPage tb in this.ListTab.TabPages)
4294             {
4295                 if (tb.Text == this.CurrentTabName)
4296                     continue;
4297
4298                 if (tb.Tag != null && tb.Controls.Count > 0)
4299                 {
4300                     var lst = (DetailsListView)tb.Tag;
4301                     for (var i = 0; i < lst.Columns.Count; i++)
4302                     {
4303                         lst.Columns[dispOrder[i]].DisplayIndex = i;
4304                         lst.Columns[i].Width = currentListView.Columns[i].Width;
4305                     }
4306                 }
4307             }
4308
4309             this.isColumnChanged = false;
4310         }
4311
4312         private void StatusText_KeyPress(object sender, KeyPressEventArgs e)
4313         {
4314             if (e.KeyChar == '@')
4315             {
4316                 if (!SettingManager.Common.UseAtIdSupplement) return;
4317                 // @マーク
4318                 var cnt = this.AtIdSupl.ItemCount;
4319                 this.ShowSuplDialog(this.StatusText, this.AtIdSupl);
4320                 if (cnt != this.AtIdSupl.ItemCount)
4321                     this.MarkSettingAtIdModified();
4322                 e.Handled = true;
4323             }
4324             else if (e.KeyChar == '#')
4325             {
4326                 if (!SettingManager.Common.UseHashSupplement) return;
4327                 this.ShowSuplDialog(this.StatusText, this.HashSupl);
4328                 e.Handled = true;
4329             }
4330         }
4331
4332         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog)
4333             => this.ShowSuplDialog(owner, dialog, 0, "");
4334
4335         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset)
4336             => this.ShowSuplDialog(owner, dialog, offset, "");
4337
4338         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset, string startswith)
4339         {
4340             dialog.StartsWith = startswith;
4341             if (dialog.Visible)
4342             {
4343                 dialog.Focus();
4344             }
4345             else
4346             {
4347                 dialog.ShowDialog();
4348             }
4349             this.TopMost = SettingManager.Common.AlwaysTop;
4350             var selStart = owner.SelectionStart;
4351             var fHalf = "";
4352             var eHalf = "";
4353             if (dialog.DialogResult == DialogResult.OK)
4354             {
4355                 if (!MyCommon.IsNullOrEmpty(dialog.InputText))
4356                 {
4357                     if (selStart > 0)
4358                     {
4359                         fHalf = owner.Text.Substring(0, selStart - offset);
4360                     }
4361                     if (selStart < owner.Text.Length)
4362                     {
4363                         eHalf = owner.Text.Substring(selStart);
4364                     }
4365                     owner.Text = fHalf + dialog.InputText + eHalf;
4366                     owner.SelectionStart = selStart + dialog.InputText.Length;
4367                 }
4368             }
4369             else
4370             {
4371                 if (selStart > 0)
4372                 {
4373                     fHalf = owner.Text.Substring(0, selStart);
4374                 }
4375                 if (selStart < owner.Text.Length)
4376                 {
4377                     eHalf = owner.Text.Substring(selStart);
4378                 }
4379                 owner.Text = fHalf + eHalf;
4380                 if (selStart > 0)
4381                 {
4382                     owner.SelectionStart = selStart;
4383                 }
4384             }
4385             owner.Focus();
4386         }
4387
4388         private void StatusText_KeyUp(object sender, KeyEventArgs e)
4389         {
4390             // スペースキーで未読ジャンプ
4391             if (!e.Alt && !e.Control && !e.Shift)
4392             {
4393                 if (e.KeyCode == Keys.Space || e.KeyCode == Keys.ProcessKey)
4394                 {
4395                     var isSpace = false;
4396                     foreach (var c in this.StatusText.Text)
4397                     {
4398                         if (c == ' ' || c == ' ')
4399                         {
4400                             isSpace = true;
4401                         }
4402                         else
4403                         {
4404                             isSpace = false;
4405                             break;
4406                         }
4407                     }
4408                     if (isSpace)
4409                     {
4410                         e.Handled = true;
4411                         this.StatusText.Text = "";
4412                         this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
4413                     }
4414                 }
4415             }
4416             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
4417         }
4418
4419         private void StatusText_TextChanged(object sender, EventArgs e)
4420         {
4421             // 文字数カウント
4422             var pLen = this.GetRestStatusCount(this.FormatStatusTextExtended(this.StatusText.Text));
4423             this.lblLen.Text = pLen.ToString();
4424             if (pLen < 0)
4425             {
4426                 this.StatusText.ForeColor = Color.Red;
4427             }
4428             else
4429             {
4430                 this.StatusText.ForeColor = this.clInputFont;
4431             }
4432
4433             this.StatusText.AccessibleDescription = string.Format(Properties.Resources.StatusText_AccessibleDescription, pLen);
4434
4435             if (MyCommon.IsNullOrEmpty(this.StatusText.Text))
4436             {
4437                 this.inReplyTo = null;
4438             }
4439         }
4440
4441         /// <summary>
4442         /// メンション以外の文字列が含まれていないテキストであるか判定します
4443         /// </summary>
4444         internal static bool TextContainsOnlyMentions(string text)
4445         {
4446             var mentions = TweetExtractor.ExtractMentionEntities(text).OrderBy(x => x.Indices[0]);
4447             var startIndex = 0;
4448
4449             foreach (var mention in mentions)
4450             {
4451                 var textPart = text.Substring(startIndex, mention.Indices[0] - startIndex);
4452
4453                 if (!string.IsNullOrWhiteSpace(textPart))
4454                     return false;
4455
4456                 startIndex = mention.Indices[1];
4457             }
4458
4459             var textPartLast = text.Substring(startIndex);
4460
4461             if (!string.IsNullOrWhiteSpace(textPartLast))
4462                 return false;
4463
4464             return true;
4465         }
4466
4467         /// <summary>
4468         /// 投稿時に auto_populate_reply_metadata オプションによって自動で追加されるメンションを除去します
4469         /// </summary>
4470         private string RemoveAutoPopuratedMentions(string statusText, out long[] autoPopulatedUserIds)
4471         {
4472             var autoPopulatedUserIdList = new List<long>();
4473
4474             var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null;
4475             if (replyToPost != null)
4476             {
4477                 if (statusText.StartsWith($"@{replyToPost.ScreenName} ", StringComparison.Ordinal))
4478                 {
4479                     statusText = statusText.Substring(replyToPost.ScreenName.Length + 2);
4480                     autoPopulatedUserIdList.Add(replyToPost.UserId);
4481
4482                     foreach (var (userId, screenName) in replyToPost.ReplyToList)
4483                     {
4484                         if (statusText.StartsWith($"@{screenName} ", StringComparison.Ordinal))
4485                         {
4486                             statusText = statusText.Substring(screenName.Length + 2);
4487                             autoPopulatedUserIdList.Add(userId);
4488                         }
4489                     }
4490                 }
4491             }
4492
4493             autoPopulatedUserIds = autoPopulatedUserIdList.ToArray();
4494
4495             return statusText;
4496         }
4497
4498         /// <summary>
4499         /// attachment_url に指定可能な URL が含まれていれば除去
4500         /// </summary>
4501         private string RemoveAttachmentUrl(string statusText, out string? attachmentUrl)
4502         {
4503             attachmentUrl = null;
4504
4505             // attachment_url は media_id と同時に使用できない
4506             if (this.ImageSelector.Visible && this.ImageSelector.SelectedService is TwitterPhoto)
4507                 return statusText;
4508
4509             var match = Twitter.AttachmentUrlRegex.Match(statusText);
4510             if (!match.Success)
4511                 return statusText;
4512
4513             attachmentUrl = match.Value;
4514
4515             // マッチした URL を空白に置換
4516             statusText = statusText.Substring(0, match.Index);
4517
4518             // テキストと URL の間にスペースが含まれていれば除去
4519             return statusText.TrimEnd(' ');
4520         }
4521
4522         private string FormatStatusTextExtended(string statusText)
4523             => this.FormatStatusTextExtended(statusText, out _, out _);
4524
4525         /// <summary>
4526         /// <see cref="FormatStatusText"/> に加えて、拡張モードで140字にカウントされない文字列の除去を行います
4527         /// </summary>
4528         private string FormatStatusTextExtended(string statusText, out long[] autoPopulatedUserIds, out string? attachmentUrl)
4529         {
4530             statusText = this.RemoveAutoPopuratedMentions(statusText, out autoPopulatedUserIds);
4531
4532             statusText = this.RemoveAttachmentUrl(statusText, out attachmentUrl);
4533
4534             return this.FormatStatusText(statusText);
4535         }
4536
4537         /// <summary>
4538         /// ツイート投稿前のフッター付与などの前処理を行います
4539         /// </summary>
4540         private string FormatStatusText(string statusText)
4541         {
4542             statusText = statusText.Replace("\r\n", "\n");
4543
4544             if (this.urlMultibyteSplit)
4545             {
4546                 // URLと全角文字の切り離し
4547                 statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& ");
4548             }
4549
4550             if (SettingManager.Common.WideSpaceConvert)
4551             {
4552                 // 文中の全角スペースを半角スペース1個にする
4553                 statusText = statusText.Replace(" ", " ");
4554             }
4555
4556             // DM の場合はこれ以降の処理を行わない
4557             if (statusText.StartsWith("D ", StringComparison.OrdinalIgnoreCase))
4558                 return statusText;
4559
4560             bool disableFooter;
4561             if (SettingManager.Common.PostShiftEnter)
4562             {
4563                 disableFooter = MyCommon.IsKeyDown(Keys.Control);
4564             }
4565             else
4566             {
4567                 if (this.StatusText.Multiline && !SettingManager.Common.PostCtrlEnter)
4568                     disableFooter = MyCommon.IsKeyDown(Keys.Control);
4569                 else
4570                     disableFooter = MyCommon.IsKeyDown(Keys.Shift);
4571             }
4572
4573             if (statusText.Contains("RT @"))
4574                 disableFooter = true;
4575
4576             // 自分宛のリプライの場合は先頭の「@screen_name 」の部分を除去する (in_reply_to_status_id は維持される)
4577             if (this.inReplyTo != null && this.inReplyTo.Value.ScreenName == this.tw.Username)
4578             {
4579                 var mentionSelf = $"@{this.tw.Username} ";
4580                 if (statusText.StartsWith(mentionSelf, StringComparison.OrdinalIgnoreCase))
4581                 {
4582                     if (statusText.Length > mentionSelf.Length || this.GetSelectedImageService() != null)
4583                         statusText = statusText.Substring(mentionSelf.Length);
4584                 }
4585             }
4586
4587             var header = "";
4588             var footer = "";
4589
4590             var hashtag = this.HashMgr.UseHash;
4591             if (!MyCommon.IsNullOrEmpty(hashtag) && !(this.HashMgr.IsNotAddToAtReply && this.inReplyTo != null))
4592             {
4593                 if (this.HashMgr.IsHead)
4594                     header = this.HashMgr.UseHash + " ";
4595                 else
4596                     footer = " " + this.HashMgr.UseHash;
4597             }
4598
4599             if (!disableFooter)
4600             {
4601                 if (SettingManager.Local.UseRecommendStatus)
4602                 {
4603                     // 推奨ステータスを使用する
4604                     footer += this.recommendedStatusFooter;
4605                 }
4606                 else if (!MyCommon.IsNullOrEmpty(SettingManager.Local.StatusText))
4607                 {
4608                     // テキストボックスに入力されている文字列を使用する
4609                     footer += " " + SettingManager.Local.StatusText.Trim();
4610                 }
4611             }
4612
4613             statusText = header + statusText + footer;
4614
4615             if (this.preventSmsCommand)
4616             {
4617                 // ツイートが意図せず SMS コマンドとして解釈されることを回避 (D, DM, M のみ)
4618                 // 参照: https://support.twitter.com/articles/14020
4619
4620                 if (Regex.IsMatch(statusText, @"^[+\-\[\]\s\\.,*/(){}^~|='&%$#""<>?]*(d|dm|m)([+\-\[\]\s\\.,*/(){}^~|='&%$#""<>?]+|$)", RegexOptions.IgnoreCase)
4621                     && !Twitter.DMSendTextRegex.IsMatch(statusText))
4622                 {
4623                     // U+200B (ZERO WIDTH SPACE) を先頭に加えて回避
4624                     statusText = '\u200b' + statusText;
4625                 }
4626             }
4627
4628             return statusText;
4629         }
4630
4631         /// <summary>
4632         /// 投稿欄に表示する入力可能な文字数を計算します
4633         /// </summary>
4634         private int GetRestStatusCount(string statusText)
4635         {
4636             var remainCount = this.tw.GetTextLengthRemain(statusText);
4637
4638             var uploadService = this.GetSelectedImageService();
4639             if (uploadService != null)
4640             {
4641                 // TODO: ImageSelector で選択中の画像の枚数が mediaCount 引数に渡るようにする
4642                 remainCount -= uploadService.GetReservedTextLength(1);
4643             }
4644
4645             return remainCount;
4646         }
4647
4648         private IMediaUploadService? GetSelectedImageService()
4649             => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null;
4650
4651         private void MyList_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
4652         {
4653             if (sender != this.CurrentListView)
4654                 return;
4655
4656             var listCache = this.listItemCache;
4657             if (listCache?.TargetList == sender && listCache.IsSupersetOf(e.StartIndex, e.EndIndex))
4658             {
4659                 // If the newly requested cache is a subset of the old cache,
4660                 // no need to rebuild everything, so do nothing.
4661                 return;
4662             }
4663
4664             // Now we need to rebuild the cache.
4665             this.CreateCache(e.StartIndex, e.EndIndex);
4666         }
4667
4668         private void MyList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
4669         {
4670             var listCache = this.listItemCache;
4671             if (listCache?.TargetList == sender)
4672             {
4673                 if (listCache.TryGetValue(e.ItemIndex, out var item, out _))
4674                 {
4675                     e.Item = item;
4676                     return;
4677                 }
4678             }
4679
4680             // A cache miss, so create a new ListViewItem and pass it back.
4681             var tabPage = (TabPage)((DetailsListView)sender).Parent;
4682             var tab = this.statuses.Tabs[tabPage.Text];
4683             try
4684             {
4685                 e.Item = this.CreateItem(tab, tab[e.ItemIndex]);
4686             }
4687             catch (Exception)
4688             {
4689                 // 不正な要求に対する間に合わせの応答
4690                 string[] sitem = { "", "", "", "", "", "", "", "" };
4691                 e.Item = new ImageListViewItem(sitem);
4692             }
4693         }
4694
4695         private void CreateCache(int startIndex, int endIndex)
4696         {
4697             var tabInfo = this.CurrentTab;
4698
4699             if (tabInfo.AllCount == 0)
4700                 return;
4701
4702             // インデックスを 0...(tabInfo.AllCount - 1) の範囲内にする
4703             int FilterRange(int index)
4704                 => Math.Max(Math.Min(index, tabInfo.AllCount - 1), 0);
4705
4706             // キャッシュ要求(要求範囲±30を作成)
4707             startIndex = FilterRange(startIndex - 30);
4708             endIndex = FilterRange(endIndex + 30);
4709
4710             var cacheLength = endIndex - startIndex + 1;
4711
4712             var tab = this.CurrentTab;
4713             var posts = tabInfo[startIndex, endIndex]; // 配列で取得
4714             var listItems = Enumerable.Range(0, cacheLength)
4715                 .Select(x => this.CreateItem(tab, posts[x]))
4716                 .ToArray();
4717
4718             var listCache = new ListViewItemCache
4719             {
4720                 TargetList = this.CurrentListView,
4721                 StartIndex = startIndex,
4722                 EndIndex = endIndex,
4723                 Cache = Enumerable.Zip(listItems, posts, (x, y) => (x, y)).ToArray(),
4724             };
4725
4726             Interlocked.Exchange(ref this.listItemCache, listCache);
4727         }
4728
4729         /// <summary>
4730         /// DetailsListView のための ListViewItem のキャッシュを消去する
4731         /// </summary>
4732         private void PurgeListViewItemCache()
4733             => Interlocked.Exchange(ref this.listItemCache, null);
4734
4735         private ListViewItem CreateItem(TabModel tab, PostClass post)
4736         {
4737             var mk = new StringBuilder();
4738
4739             if (post.FavoritedCount > 0) mk.Append("+" + post.FavoritedCount);
4740
4741             var scaledIconSz = (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Width);
4742
4743             ImageListViewItem itm;
4744             if (post.RetweetedId == null)
4745             {
4746                 string[] sitem =
4747                 {
4748                     "",
4749                     post.Nickname,
4750                     post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
4751                     post.CreatedAt.ToLocalTimeString(SettingManager.Common.DateTimeFormat),
4752                     post.ScreenName,
4753                     "",
4754                     mk.ToString(),
4755                     post.Source,
4756                 };
4757                 itm = new ImageListViewItem(sitem, this.iconCache, post.ImageUrl, scaledIconSz);
4758             }
4759             else
4760             {
4761                 string[] sitem =
4762                 {
4763                     "",
4764                     post.Nickname,
4765                     post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
4766                     post.CreatedAt.ToLocalTimeString(SettingManager.Common.DateTimeFormat),
4767                     post.ScreenName + Environment.NewLine + "(RT:" + post.RetweetedBy + ")",
4768                     "",
4769                     mk.ToString(),
4770                     post.Source,
4771                 };
4772                 itm = new ImageListViewItem(sitem, this.iconCache, post.ImageUrl, scaledIconSz);
4773             }
4774             itm.StateIndex = post.StateIndex;
4775             itm.Tag = post;
4776
4777             var read = post.IsRead;
4778             // 未読管理していなかったら既読として扱う
4779             if (!tab.UnreadManage || !SettingManager.Common.UnreadManage)
4780                 read = true;
4781
4782             this.ChangeItemStyleRead(read, itm, post, null);
4783
4784             if (tab.TabName == this.CurrentTabName)
4785                 this.ColorizeList(itm, post);
4786
4787             return itm;
4788         }
4789
4790         /// <summary>
4791         /// 全てのタブの振り分けルールを反映し直します
4792         /// </summary>
4793         private void ApplyPostFilters()
4794         {
4795             using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
4796             {
4797                 this.PurgeListViewItemCache();
4798                 this.statuses.FilterAll();
4799
4800                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
4801                 {
4802                     var tabPage = this.ListTab.TabPages[index];
4803                     var listview = (DetailsListView)tabPage.Tag;
4804                     using (ControlTransaction.Update(listview))
4805                     {
4806                         listview.VirtualListSize = tab.AllCount;
4807                     }
4808
4809                     if (SettingManager.Common.TabIconDisp)
4810                     {
4811                         if (tab.UnreadCount > 0)
4812                             tabPage.ImageIndex = 0;
4813                         else
4814                             tabPage.ImageIndex = -1;
4815                     }
4816                 }
4817
4818                 if (!SettingManager.Common.TabIconDisp)
4819                     this.ListTab.Refresh();
4820
4821                 this.SetMainWindowTitle();
4822                 this.SetStatusLabelUrl();
4823             }
4824         }
4825
4826         private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
4827             => e.DrawDefault = true;
4828
4829         private void MyList_HScrolled(object sender, EventArgs e)
4830             => ((DetailsListView)sender).Refresh();
4831
4832         private void MyList_DrawItem(object sender, DrawListViewItemEventArgs e)
4833         {
4834             if (e.State == 0) return;
4835             e.DrawDefault = false;
4836
4837             SolidBrush brs2;
4838             if (!e.Item.Selected) // e.ItemStateでうまく判定できない???
4839             {
4840                 if (e.Item.BackColor == this.clSelf)
4841                     brs2 = this.brsBackColorMine;
4842                 else if (e.Item.BackColor == this.clAtSelf)
4843                     brs2 = this.brsBackColorAt;
4844                 else if (e.Item.BackColor == this.clTarget)
4845                     brs2 = this.brsBackColorYou;
4846                 else if (e.Item.BackColor == this.clAtTarget)
4847                     brs2 = this.brsBackColorAtYou;
4848                 else if (e.Item.BackColor == this.clAtFromTarget)
4849                     brs2 = this.brsBackColorAtFromTarget;
4850                 else if (e.Item.BackColor == this.clAtTo)
4851                     brs2 = this.brsBackColorAtTo;
4852                 else
4853                     brs2 = this.brsBackColorNone;
4854             }
4855             else
4856             {
4857                 // 選択中の行
4858                 if (((Control)sender).Focused)
4859                     brs2 = this.brsHighLight;
4860                 else
4861                     brs2 = this.brsDeactiveSelection;
4862             }
4863             e.Graphics.FillRectangle(brs2, e.Bounds);
4864             e.DrawFocusRectangle();
4865             this.DrawListViewItemIcon(e);
4866         }
4867
4868         private void MyList_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
4869         {
4870             if (e.ItemState == 0) return;
4871
4872             if (e.ColumnIndex > 0)
4873             {
4874                 // アイコン以外の列
4875                 var post = (PostClass)e.Item.Tag;
4876
4877                 RectangleF rct = e.Bounds;
4878                 rct.Width = e.Header.Width;
4879                 var fontHeight = e.Item.Font.Height;
4880                 if (this.iconCol)
4881                 {
4882                     rct.Y += fontHeight;
4883                     rct.Height -= fontHeight;
4884                 }
4885
4886                 var drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out var heightDiff));
4887
4888                 // フォントの高さの半分を足してるのは保険。無くてもいいかも。
4889                 if (this.iconCol || drawLineCount > 1)
4890                 {
4891                     if (heightDiff < fontHeight * 0.7)
4892                     {
4893                         // 最終行が70%以上欠けていたら、最終行は表示しない
4894                         rct.Height = (fontHeight * drawLineCount) - 1;
4895                     }
4896                     else
4897                     {
4898                         drawLineCount += 1;
4899                     }
4900                 }
4901
4902                 if (rct.Width > 0)
4903                 {
4904                     var color = (!e.Item.Selected) ? e.Item.ForeColor : // 選択されていない行
4905                         ((Control)sender).Focused ? this.clHighLight : // 選択中の行
4906                         this.clUnread;
4907
4908                     if (this.iconCol)
4909                     {
4910                         var rctB = e.Bounds;
4911                         rctB.Width = e.Header.Width;
4912                         rctB.Height = fontHeight;
4913
4914                         using var fnt = new Font(e.Item.Font, FontStyle.Bold);
4915                         var formatFlags1 = TextFormatFlags.WordBreak |
4916                             TextFormatFlags.EndEllipsis |
4917                             TextFormatFlags.GlyphOverhangPadding |
4918                             TextFormatFlags.NoPrefix;
4919
4920                         TextRenderer.DrawText(
4921                             e.Graphics,
4922                             post.IsDeleted ? "(DELETED)" : post.TextSingleLine,
4923                             e.Item.Font,
4924                             Rectangle.Round(rct),
4925                             color,
4926                             formatFlags1);
4927
4928                         var formatFlags2 = TextFormatFlags.SingleLine |
4929                             TextFormatFlags.EndEllipsis |
4930                             TextFormatFlags.GlyphOverhangPadding |
4931                             TextFormatFlags.NoPrefix;
4932
4933                         TextRenderer.DrawText(
4934                             e.Graphics,
4935                             e.Item.SubItems[4].Text + " / " + e.Item.SubItems[1].Text + " (" + e.Item.SubItems[3].Text + ") " + e.Item.SubItems[5].Text + e.Item.SubItems[6].Text + " [" + e.Item.SubItems[7].Text + "]",
4936                             fnt,
4937                             rctB,
4938                             color,
4939                             formatFlags2);
4940                     }
4941                     else
4942                     {
4943                         string text;
4944                         if (e.ColumnIndex != 2)
4945                             text = e.SubItem.Text;
4946                         else
4947                             text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine;
4948
4949                         if (drawLineCount == 1)
4950                         {
4951                             var formatFlags = TextFormatFlags.SingleLine |
4952                                 TextFormatFlags.EndEllipsis |
4953                                 TextFormatFlags.GlyphOverhangPadding |
4954                                 TextFormatFlags.NoPrefix |
4955                                 TextFormatFlags.VerticalCenter;
4956
4957                             TextRenderer.DrawText(
4958                                 e.Graphics,
4959                                 text,
4960                                 e.Item.Font,
4961                                 Rectangle.Round(rct),
4962                                 color,
4963                                 formatFlags);
4964                         }
4965                         else
4966                         {
4967                             var formatFlags = TextFormatFlags.WordBreak |
4968                                 TextFormatFlags.EndEllipsis |
4969                                 TextFormatFlags.GlyphOverhangPadding |
4970                                 TextFormatFlags.NoPrefix;
4971
4972                             TextRenderer.DrawText(
4973                                 e.Graphics,
4974                                 text,
4975                                 e.Item.Font,
4976                                 Rectangle.Round(rct),
4977                                 color,
4978                                 formatFlags);
4979                         }
4980                     }
4981                 }
4982             }
4983         }
4984
4985         private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
4986         {
4987             if (this.iconSz == 0) return;
4988
4989             var item = (ImageListViewItem)e.Item;
4990
4991             // e.Bounds.Leftが常に0を指すから自前で計算
4992             var itemRect = item.Bounds;
4993             var col0 = e.Item.ListView.Columns[0];
4994             itemRect.Width = col0.Width;
4995
4996             if (col0.DisplayIndex > 0)
4997             {
4998                 foreach (ColumnHeader clm in e.Item.ListView.Columns)
4999                 {
5000                     if (clm.DisplayIndex < col0.DisplayIndex)
5001                         itemRect.X += clm.Width;
5002                 }
5003             }
5004
5005             // ディスプレイの DPI 設定を考慮したアイコンサイズ
5006             var realIconSize = new SizeF(this.iconSz * this.CurrentScaleFactor.Width, this.iconSz * this.CurrentScaleFactor.Height).ToSize();
5007             var realStateSize = new SizeF(16 * this.CurrentScaleFactor.Width, 16 * this.CurrentScaleFactor.Height).ToSize();
5008
5009             Rectangle iconRect;
5010             var img = item.Image;
5011             if (img != null)
5012             {
5013                 iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, realIconSize), itemRect);
5014                 iconRect.Offset(0, Math.Max(0, (itemRect.Height - realIconSize.Height) / 2));
5015
5016                 if (iconRect.Width > 0)
5017                 {
5018                     e.Graphics.FillRectangle(Brushes.White, iconRect);
5019                     e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
5020                     try
5021                     {
5022                         e.Graphics.DrawImage(img.Image, iconRect);
5023                     }
5024                     catch (ArgumentException)
5025                     {
5026                         item.RefreshImageAsync();
5027                     }
5028                 }
5029             }
5030             else
5031             {
5032                 iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, new Size(1, 1)), itemRect);
5033
5034                 item.GetImageAsync();
5035             }
5036
5037             if (item.StateIndex > -1)
5038             {
5039                 var stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect);
5040                 if (stateRect.Width > 0)
5041                     e.Graphics.DrawImage(this.PostStateImageList.Images[item.StateIndex], stateRect);
5042             }
5043         }
5044
5045         protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
5046         {
5047             base.ScaleControl(factor, specified);
5048
5049             ScaleChildControl(this.TabImage, factor);
5050
5051             var tabpages = this.ListTab.TabPages.Cast<TabPage>();
5052             var listviews = tabpages.Select(x => x.Tag).Cast<ListView>();
5053
5054             foreach (var listview in listviews)
5055             {
5056                 ScaleChildControl(listview, factor);
5057             }
5058         }
5059
5060         internal void DoTabSearch(string searchWord, bool caseSensitive, bool useRegex, SEARCHTYPE searchType)
5061         {
5062             var tab = this.CurrentTab;
5063
5064             if (tab.AllCount == 0)
5065             {
5066                 MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
5067                 return;
5068             }
5069
5070             var selectedIndex = tab.SelectedIndex;
5071
5072             int startIndex;
5073             switch (searchType)
5074             {
5075                 case SEARCHTYPE.NextSearch: // 次を検索
5076                     if (selectedIndex != -1)
5077                         startIndex = Math.Min(selectedIndex + 1, tab.AllCount - 1);
5078                     else
5079                         startIndex = 0;
5080                     break;
5081                 case SEARCHTYPE.PrevSearch: // 前を検索
5082                     if (selectedIndex != -1)
5083                         startIndex = Math.Max(selectedIndex - 1, 0);
5084                     else
5085                         startIndex = tab.AllCount - 1;
5086                     break;
5087                 case SEARCHTYPE.DialogSearch: // ダイアログからの検索
5088                 default:
5089                     if (selectedIndex != -1)
5090                         startIndex = selectedIndex;
5091                     else
5092                         startIndex = 0;
5093                     break;
5094             }
5095
5096             Func<string, bool> stringComparer;
5097             try
5098             {
5099                 stringComparer = this.CreateSearchComparer(searchWord, useRegex, caseSensitive);
5100             }
5101             catch (ArgumentException)
5102             {
5103                 MessageBox.Show(Properties.Resources.DoTabSearchText1, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Error);
5104                 return;
5105             }
5106
5107             var reverse = searchType == SEARCHTYPE.PrevSearch;
5108             var foundIndex = tab.SearchPostsAll(stringComparer, startIndex, reverse)
5109                 .DefaultIfEmpty(-1).First();
5110
5111             if (foundIndex == -1)
5112             {
5113                 MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
5114                 return;
5115             }
5116
5117             var listView = this.CurrentListView;
5118             this.SelectListItem(listView, foundIndex);
5119             listView.EnsureVisible(foundIndex);
5120         }
5121
5122         private void MenuItemSubSearch_Click(object sender, EventArgs e)
5123             => this.ShowSearchDialog(); // 検索メニュー
5124
5125         private void MenuItemSearchNext_Click(object sender, EventArgs e)
5126         {
5127             var previousSearch = this.SearchDialog.ResultOptions;
5128             if (previousSearch == null || previousSearch.Type != SearchWordDialog.SearchType.Timeline)
5129             {
5130                 this.SearchDialog.Reset();
5131                 this.ShowSearchDialog();
5132                 return;
5133             }
5134
5135             // 次を検索
5136             this.DoTabSearch(
5137                 previousSearch.Query,
5138                 previousSearch.CaseSensitive,
5139                 previousSearch.UseRegex,
5140                 SEARCHTYPE.NextSearch);
5141         }
5142
5143         private void MenuItemSearchPrev_Click(object sender, EventArgs e)
5144         {
5145             var previousSearch = this.SearchDialog.ResultOptions;
5146             if (previousSearch == null || previousSearch.Type != SearchWordDialog.SearchType.Timeline)
5147             {
5148                 this.SearchDialog.Reset();
5149                 this.ShowSearchDialog();
5150                 return;
5151             }
5152
5153             // 前を検索
5154             this.DoTabSearch(
5155                 previousSearch.Query,
5156                 previousSearch.CaseSensitive,
5157                 previousSearch.UseRegex,
5158                 SEARCHTYPE.PrevSearch);
5159         }
5160
5161         /// <summary>
5162         /// 検索ダイアログを表示し、検索を実行します
5163         /// </summary>
5164         private void ShowSearchDialog()
5165         {
5166             if (this.SearchDialog.ShowDialog(this) != DialogResult.OK)
5167             {
5168                 this.TopMost = SettingManager.Common.AlwaysTop;
5169                 return;
5170             }
5171             this.TopMost = SettingManager.Common.AlwaysTop;
5172
5173             var searchOptions = this.SearchDialog.ResultOptions!;
5174             if (searchOptions.Type == SearchWordDialog.SearchType.Timeline)
5175             {
5176                 if (searchOptions.NewTab)
5177                 {
5178                     var tabName = Properties.Resources.SearchResults_TabName;
5179
5180                     try
5181                     {
5182                         tabName = this.statuses.MakeTabName(tabName);
5183                     }
5184                     catch (TabException ex)
5185                     {
5186                         MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
5187                     }
5188
5189                     var resultTab = new LocalSearchTabModel(tabName);
5190                     this.AddNewTab(resultTab, startup: false);
5191                     this.statuses.AddTab(resultTab);
5192
5193                     var targetTab = this.CurrentTab;
5194
5195                     Func<string, bool> stringComparer;
5196                     try
5197                     {
5198                         stringComparer = this.CreateSearchComparer(searchOptions.Query, searchOptions.UseRegex, searchOptions.CaseSensitive);
5199                     }
5200                     catch (ArgumentException)
5201                     {
5202                         MessageBox.Show(Properties.Resources.DoTabSearchText1, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Error);
5203                         return;
5204                     }
5205
5206                     var foundIndices = targetTab.SearchPostsAll(stringComparer).ToArray();
5207                     if (foundIndices.Length == 0)
5208                     {
5209                         MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
5210                         return;
5211                     }
5212
5213                     var foundPosts = foundIndices.Select(x => targetTab[x]);
5214                     foreach (var post in foundPosts)
5215                     {
5216                         resultTab.AddPostQueue(post);
5217                     }
5218
5219                     this.statuses.DistributePosts();
5220                     this.RefreshTimeline();
5221
5222                     var tabIndex = this.statuses.Tabs.IndexOf(tabName);
5223                     this.ListTab.SelectedIndex = tabIndex;
5224                 }
5225                 else
5226                 {
5227                     this.DoTabSearch(
5228                         searchOptions.Query,
5229                         searchOptions.CaseSensitive,
5230                         searchOptions.UseRegex,
5231                         SEARCHTYPE.DialogSearch);
5232                 }
5233             }
5234             else if (searchOptions.Type == SearchWordDialog.SearchType.Public)
5235             {
5236                 this.AddNewTabForSearch(searchOptions.Query);
5237             }
5238         }
5239
5240         /// <summary>発言検索に使用するメソッドを生成します</summary>
5241         /// <exception cref="ArgumentException">
5242         /// <paramref name="useRegex"/> が true かつ、<paramref name="query"/> が不正な正規表現な場合
5243         /// </exception>
5244         private Func<string, bool> CreateSearchComparer(string query, bool useRegex, bool caseSensitive)
5245         {
5246             if (useRegex)
5247             {
5248                 var regexOption = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase;
5249                 var regex = new Regex(query, regexOption);
5250
5251                 return x => regex.IsMatch(x);
5252             }
5253             else
5254             {
5255                 var comparisonType = caseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase;
5256
5257                 return x => x.IndexOf(query, comparisonType) != -1;
5258             }
5259         }
5260
5261         private void AboutMenuItem_Click(object sender, EventArgs e)
5262         {
5263             using (var about = new TweenAboutBox())
5264             {
5265                 about.ShowDialog(this);
5266             }
5267             this.TopMost = SettingManager.Common.AlwaysTop;
5268         }
5269
5270         private void JumpUnreadMenuItem_Click(object sender, EventArgs e)
5271         {
5272             var bgnIdx = this.statuses.SelectedTabIndex;
5273
5274             if (this.ImageSelector.Enabled)
5275                 return;
5276
5277             TabModel? foundTab = null;
5278             var foundIndex = 0;
5279
5280             // 現在タブから最終タブまで探索
5281             foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Skip(bgnIdx))
5282             {
5283                 var unreadIndex = tab.NextUnreadIndex;
5284                 if (unreadIndex != -1)
5285                 {
5286                     this.ListTab.SelectedIndex = index;
5287                     foundTab = tab;
5288                     foundIndex = unreadIndex;
5289                     break;
5290                 }
5291             }
5292
5293             // 未読みつからず&現在タブが先頭ではなかったら、先頭タブから現在タブの手前まで探索
5294             if (foundTab == null && bgnIdx > 0)
5295             {
5296                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Take(bgnIdx))
5297                 {
5298                     var unreadIndex = tab.NextUnreadIndex;
5299                     if (unreadIndex != -1)
5300                     {
5301                         this.ListTab.SelectedIndex = index;
5302                         foundTab = tab;
5303                         foundIndex = unreadIndex;
5304                         break;
5305                     }
5306                 }
5307             }
5308
5309             DetailsListView lst;
5310
5311             if (foundTab == null)
5312             {
5313                 // 全部調べたが未読見つからず→先頭タブの最新発言へ
5314                 this.ListTab.SelectedIndex = 0;
5315                 var tabPage = this.ListTab.TabPages[0];
5316                 var tab = this.statuses.Tabs[0];
5317
5318                 if (tab.AllCount == 0)
5319                     return;
5320
5321                 if (this.statuses.SortOrder == SortOrder.Ascending)
5322                     foundIndex = tab.AllCount - 1;
5323                 else
5324                     foundIndex = 0;
5325
5326                 lst = (DetailsListView)tabPage.Tag;
5327             }
5328             else
5329             {
5330                 var foundTabIndex = this.statuses.Tabs.IndexOf(foundTab);
5331                 lst = (DetailsListView)this.ListTab.TabPages[foundTabIndex].Tag;
5332             }
5333
5334             this.SelectListItem(lst, foundIndex);
5335
5336             if (this.statuses.SortMode == ComparerMode.Id)
5337             {
5338                 if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - this.iconSz - 10 ||
5339                     this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < this.iconSz + 10)
5340                 {
5341                     this.MoveTop();
5342                 }
5343                 else
5344                 {
5345                     lst.EnsureVisible(foundIndex);
5346                 }
5347             }
5348             else
5349             {
5350                 lst.EnsureVisible(foundIndex);
5351             }
5352
5353             lst.Focus();
5354         }
5355
5356         private async void StatusOpenMenuItem_Click(object sender, EventArgs e)
5357         {
5358             var tab = this.CurrentTab;
5359             var post = this.CurrentPost;
5360             if (post != null && tab.TabType != MyCommon.TabUsageType.DirectMessage)
5361                 await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(post));
5362         }
5363
5364         private async void VerUpMenuItem_Click(object sender, EventArgs e)
5365             => await this.CheckNewVersion(false);
5366
5367         private void RunTweenUp()
5368         {
5369             var pinfo = new ProcessStartInfo
5370             {
5371                 UseShellExecute = true,
5372                 WorkingDirectory = MyCommon.SettingPath,
5373                 FileName = Path.Combine(MyCommon.SettingPath, "TweenUp3.exe"),
5374                 Arguments = "\"" + Application.StartupPath + "\"",
5375             };
5376
5377             try
5378             {
5379                 Process.Start(pinfo);
5380             }
5381             catch (Exception)
5382             {
5383                 MessageBox.Show("Failed to execute TweenUp3.exe.");
5384             }
5385         }
5386
5387         public class VersionInfo
5388         {
5389             public Version Version { get; }
5390
5391             public Uri DownloadUri { get; }
5392
5393             public string ReleaseNote { get; }
5394
5395             public VersionInfo(Version version, Uri downloadUri, string releaseNote)
5396                 => (this.Version, this.DownloadUri, this.ReleaseNote) = (version, downloadUri, releaseNote);
5397         }
5398
5399         /// <summary>
5400         /// OpenTween の最新バージョンの情報を取得します
5401         /// </summary>
5402         public async Task<VersionInfo> GetVersionInfoAsync()
5403         {
5404             var versionInfoUrl = new Uri(ApplicationSettings.VersionInfoUrl + "?" +
5405                 DateTimeUtc.Now.ToString("yyMMddHHmmss") + Environment.TickCount);
5406
5407             var responseText = await Networking.Http.GetStringAsync(versionInfoUrl)
5408                 .ConfigureAwait(false);
5409
5410             // 改行2つで前後パートを分割(前半がバージョン番号など、後半が詳細テキスト)
5411             var msgPart = responseText.Split(new[] { "\n\n", "\r\n\r\n" }, 2, StringSplitOptions.None);
5412
5413             var msgHeader = msgPart[0].Split(new[] { "\n", "\r\n" }, StringSplitOptions.None);
5414             var msgBody = msgPart.Length == 2 ? msgPart[1] : "";
5415
5416             msgBody = Regex.Replace(msgBody, "(?<!\r)\n", "\r\n"); // LF -> CRLF
5417
5418             return new VersionInfo(
5419                 version: Version.Parse(msgHeader[0]),
5420                 downloadUri: new Uri(msgHeader[1]),
5421                 releaseNote: msgBody
5422             );
5423         }
5424
5425         private async Task CheckNewVersion(bool startup = false)
5426         {
5427             if (ApplicationSettings.VersionInfoUrl == null)
5428                 return; // 更新チェック無効化
5429
5430             try
5431             {
5432                 var versionInfo = await this.GetVersionInfoAsync();
5433
5434                 if (versionInfo.Version <= Version.Parse(MyCommon.FileVersion))
5435                 {
5436                     // 更新不要
5437                     if (!startup)
5438                     {
5439                         var msgtext = string.Format(
5440                             Properties.Resources.CheckNewVersionText7,
5441                             MyCommon.GetReadableVersion(),
5442                             MyCommon.GetReadableVersion(versionInfo.Version));
5443                         msgtext = MyCommon.ReplaceAppName(msgtext);
5444
5445                         MessageBox.Show(
5446                             msgtext,
5447                             MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2),
5448                             MessageBoxButtons.OK,
5449                             MessageBoxIcon.Information);
5450                     }
5451                     return;
5452                 }
5453
5454                 if (startup && versionInfo.Version <= SettingManager.Common.SkipUpdateVersion)
5455                     return;
5456
5457                 using var dialog = new UpdateDialog();
5458
5459                 dialog.SummaryText = string.Format(Properties.Resources.CheckNewVersionText3,
5460                     MyCommon.GetReadableVersion(versionInfo.Version));
5461                 dialog.DetailsText = versionInfo.ReleaseNote;
5462
5463                 if (dialog.ShowDialog(this) == DialogResult.Yes)
5464                 {
5465                     await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri.OriginalString);
5466                 }
5467                 else if (dialog.SkipButtonPressed)
5468                 {
5469                     SettingManager.Common.SkipUpdateVersion = versionInfo.Version;
5470                     this.MarkSettingCommonModified();
5471                 }
5472             }
5473             catch (Exception)
5474             {
5475                 this.StatusLabel.Text = Properties.Resources.CheckNewVersionText9;
5476                 if (!startup)
5477                 {
5478                     MessageBox.Show(
5479                         Properties.Resources.CheckNewVersionText10,
5480                         MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2),
5481                         MessageBoxButtons.OK,
5482                         MessageBoxIcon.Exclamation,
5483                         MessageBoxDefaultButton.Button2);
5484                 }
5485             }
5486         }
5487
5488         private void UpdateSelectedPost()
5489         {
5490             // 件数関連の場合、タイトル即時書き換え
5491             if (SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.None &&
5492                SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Post &&
5493                SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver &&
5494                SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus)
5495             {
5496                 this.SetMainWindowTitle();
5497             }
5498             if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.OrdinalIgnoreCase))
5499                 this.SetStatusLabelUrl();
5500
5501             if (SettingManager.Common.TabIconDisp)
5502             {
5503                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
5504                 {
5505                     if (tab.UnreadCount == 0)
5506                     {
5507                         var tabPage = this.ListTab.TabPages[index];
5508                         if (tabPage.ImageIndex == 0)
5509                             tabPage.ImageIndex = -1;
5510                     }
5511                 }
5512             }
5513             else
5514             {
5515                 this.ListTab.Refresh();
5516             }
5517
5518             this.DispSelectedPost();
5519         }
5520
5521         public string CreateDetailHtml(string orgdata)
5522             => this.detailHtmlFormatPreparedTemplate.Replace("%CONTENT_HTML%", orgdata);
5523
5524         private void DispSelectedPost()
5525             => this.DispSelectedPost(false);
5526
5527         private PostClass displayPost = new PostClass();
5528
5529         /// <summary>
5530         /// サムネイル表示に使用する CancellationToken の生成元
5531         /// </summary>
5532         private CancellationTokenSource? thumbnailTokenSource = null;
5533
5534         private void DispSelectedPost(bool forceupdate)
5535         {
5536             var currentPost = this.CurrentPost;
5537             if (currentPost == null)
5538                 return;
5539
5540             var oldDisplayPost = this.displayPost;
5541             this.displayPost = currentPost;
5542
5543             if (!forceupdate && currentPost.Equals(oldDisplayPost))
5544                 return;
5545
5546             var loadTasks = new List<Task>
5547             {
5548                 this.tweetDetailsView.ShowPostDetails(currentPost),
5549             };
5550
5551             this.SplitContainer3.Panel2Collapsed = true;
5552
5553             if (SettingManager.Common.PreviewEnable)
5554             {
5555                 var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource());
5556                 oldTokenSource?.Cancel();
5557
5558                 var token = this.thumbnailTokenSource!.Token;
5559                 loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token));
5560             }
5561
5562             async Task DelayedTasks()
5563             {
5564                 try
5565                 {
5566                     await Task.WhenAll(loadTasks);
5567                 }
5568                 catch (OperationCanceledException)
5569                 {
5570                 }
5571             }
5572
5573             // サムネイルの読み込みを待たずに次に選択されたツイートを表示するため await しない
5574             _ = DelayedTasks();
5575         }
5576
5577         private async void MatomeMenuItem_Click(object sender, EventArgs e)
5578             => await this.OpenApplicationWebsite();
5579
5580         private async Task OpenApplicationWebsite()
5581             => await MyCommon.OpenInBrowserAsync(this, ApplicationSettings.WebsiteUrl);
5582
5583         private async void ShortcutKeyListMenuItem_Click(object sender, EventArgs e)
5584             => await MyCommon.OpenInBrowserAsync(this, ApplicationSettings.ShortcutKeyUrl);
5585
5586         private async void ListTab_KeyDown(object sender, KeyEventArgs e)
5587         {
5588             var tab = this.CurrentTab;
5589             if (tab.TabType == MyCommon.TabUsageType.PublicSearch)
5590             {
5591                 var pnl = this.CurrentTabPage.Controls["panelSearch"];
5592                 if (pnl.Controls["comboSearch"].Focused ||
5593                     pnl.Controls["comboLang"].Focused ||
5594                     pnl.Controls["buttonSearch"].Focused) return;
5595             }
5596
5597             if (e.Control || e.Shift || e.Alt)
5598                 this.anchorFlag = false;
5599
5600             if (this.CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask))
5601             {
5602                 e.Handled = true;
5603                 e.SuppressKeyPress = true;
5604             }
5605
5606             if (asyncTask != null)
5607                 await asyncTask;
5608         }
5609
5610         private ShortcutCommand[] shortcutCommands = Array.Empty<ShortcutCommand>();
5611
5612         private void InitializeShortcuts()
5613         {
5614             this.shortcutCommands = new[]
5615             {
5616                 // リストのカーソル移動関係(上下キー、PageUp/Downに該当)
5617                 ShortcutCommand.Create(Keys.J, Keys.Control | Keys.J, Keys.Shift | Keys.J, Keys.Control | Keys.Shift | Keys.J)
5618                     .FocusedOn(FocusedControl.ListTab)
5619                     .Do(() => SendKeys.Send("{DOWN}")),
5620
5621                 ShortcutCommand.Create(Keys.K, Keys.Control | Keys.K, Keys.Shift | Keys.K, Keys.Control | Keys.Shift | Keys.K)
5622                     .FocusedOn(FocusedControl.ListTab)
5623                     .Do(() => SendKeys.Send("{UP}")),
5624
5625                 ShortcutCommand.Create(Keys.F, Keys.Shift | Keys.F)
5626                     .FocusedOn(FocusedControl.ListTab)
5627                     .Do(() => SendKeys.Send("{PGDN}")),
5628
5629                 ShortcutCommand.Create(Keys.B, Keys.Shift | Keys.B)
5630                     .FocusedOn(FocusedControl.ListTab)
5631                     .Do(() => SendKeys.Send("{PGUP}")),
5632
5633                 ShortcutCommand.Create(Keys.F1)
5634                     .Do(() => this.OpenApplicationWebsite()),
5635
5636                 ShortcutCommand.Create(Keys.F3)
5637                     .Do(() => this.MenuItemSearchNext_Click(this.MenuItemSearchNext, EventArgs.Empty)),
5638
5639                 ShortcutCommand.Create(Keys.F5)
5640                     .Do(() => this.DoRefresh()),
5641
5642                 ShortcutCommand.Create(Keys.F6)
5643                     .Do(() => this.RefreshTabAsync<MentionsTabModel>()),
5644
5645                 ShortcutCommand.Create(Keys.F7)
5646                     .Do(() => this.RefreshTabAsync<DirectMessagesTabModel>()),
5647
5648                 ShortcutCommand.Create(Keys.Space, Keys.ProcessKey)
5649                     .NotFocusedOn(FocusedControl.StatusText)
5650                     .Do(() =>
5651                     {
5652                         this.anchorFlag = false;
5653                         this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
5654                     }),
5655
5656                 ShortcutCommand.Create(Keys.G)
5657                     .NotFocusedOn(FocusedControl.StatusText)
5658                     .Do(() =>
5659                     {
5660                         this.anchorFlag = false;
5661                         this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
5662                     }),
5663
5664                 ShortcutCommand.Create(Keys.Right, Keys.N)
5665                     .FocusedOn(FocusedControl.ListTab)
5666                     .Do(() => this.GoRelPost(forward: true)),
5667
5668                 ShortcutCommand.Create(Keys.Left, Keys.P)
5669                     .FocusedOn(FocusedControl.ListTab)
5670                     .Do(() => this.GoRelPost(forward: false)),
5671
5672                 ShortcutCommand.Create(Keys.OemPeriod)
5673                     .FocusedOn(FocusedControl.ListTab)
5674                     .Do(() => this.GoAnchor()),
5675
5676                 ShortcutCommand.Create(Keys.I)
5677                     .FocusedOn(FocusedControl.ListTab)
5678                     .OnlyWhen(() => this.StatusText.Enabled)
5679                     .Do(() => this.StatusText.Focus()),
5680
5681                 ShortcutCommand.Create(Keys.Enter)
5682                     .FocusedOn(FocusedControl.ListTab)
5683                     .Do(() => this.ListItemDoubleClickAction()),
5684
5685                 ShortcutCommand.Create(Keys.R)
5686                     .FocusedOn(FocusedControl.ListTab)
5687                     .Do(() => this.DoRefresh()),
5688
5689                 ShortcutCommand.Create(Keys.L)
5690                     .FocusedOn(FocusedControl.ListTab)
5691                     .Do(() =>
5692                     {
5693                         this.anchorFlag = false;
5694                         this.GoPost(forward: true);
5695                     }),
5696
5697                 ShortcutCommand.Create(Keys.H)
5698                     .FocusedOn(FocusedControl.ListTab)
5699                     .Do(() =>
5700                     {
5701                         this.anchorFlag = false;
5702                         this.GoPost(forward: false);
5703                     }),
5704
5705                 ShortcutCommand.Create(Keys.Z, Keys.Oemcomma)
5706                     .FocusedOn(FocusedControl.ListTab)
5707                     .Do(() =>
5708                     {
5709                         this.anchorFlag = false;
5710                         this.MoveTop();
5711                     }),
5712
5713                 ShortcutCommand.Create(Keys.S)
5714                     .FocusedOn(FocusedControl.ListTab)
5715                     .Do(() =>
5716                     {
5717                         this.anchorFlag = false;
5718                         this.GoNextTab(forward: true);
5719                     }),
5720
5721                 ShortcutCommand.Create(Keys.A)
5722                     .FocusedOn(FocusedControl.ListTab)
5723                     .Do(() =>
5724                     {
5725                         this.anchorFlag = false;
5726                         this.GoNextTab(forward: false);
5727                     }),
5728
5729                 // ] in_reply_to参照元へ戻る
5730                 ShortcutCommand.Create(Keys.Oem4)
5731                     .FocusedOn(FocusedControl.ListTab)
5732                     .Do(() =>
5733                     {
5734                         this.anchorFlag = false;
5735                         return this.GoInReplyToPostTree();
5736                     }),
5737
5738                 // [ in_reply_toへジャンプ
5739                 ShortcutCommand.Create(Keys.Oem6)
5740                     .FocusedOn(FocusedControl.ListTab)
5741                     .Do(() =>
5742                     {
5743                         this.anchorFlag = false;
5744                         this.GoBackInReplyToPostTree();
5745                     }),
5746
5747                 ShortcutCommand.Create(Keys.Escape)
5748                     .FocusedOn(FocusedControl.ListTab)
5749                     .Do(() =>
5750                     {
5751                         this.anchorFlag = false;
5752                         var tab = this.CurrentTab;
5753                         var tabtype = tab.TabType;
5754                         if (tabtype == MyCommon.TabUsageType.Related || tabtype == MyCommon.TabUsageType.UserTimeline || tabtype == MyCommon.TabUsageType.PublicSearch || tabtype == MyCommon.TabUsageType.SearchResults)
5755                         {
5756                             this.RemoveSpecifiedTab(tab.TabName, false);
5757                             this.SaveConfigsTabs();
5758                         }
5759                     }),
5760
5761                 // 上下キー, PageUp/Downキー, Home/Endキー は既定の動作を残しつつアンカー初期化
5762                 ShortcutCommand.Create(Keys.Up, Keys.Down, Keys.PageUp, Keys.PageDown, Keys.Home, Keys.End)
5763                     .FocusedOn(FocusedControl.ListTab)
5764                     .Do(() => this.anchorFlag = false, preventDefault: false),
5765
5766                 // PreviewKeyDownEventArgs.IsInputKey を true にしてスクロールを発生させる
5767                 ShortcutCommand.Create(Keys.Up, Keys.Down)
5768                     .FocusedOn(FocusedControl.PostBrowser)
5769                     .Do(() => { }),
5770
5771                 ShortcutCommand.Create(Keys.Control | Keys.R)
5772                     .Do(() => this.MakeReplyText()),
5773
5774                 ShortcutCommand.Create(Keys.Control | Keys.D)
5775                     .Do(() => this.DoStatusDelete()),
5776
5777                 ShortcutCommand.Create(Keys.Control | Keys.M)
5778                     .Do(() => this.MakeDirectMessageText()),
5779
5780                 ShortcutCommand.Create(Keys.Control | Keys.S)
5781                     .Do(() => this.FavoriteChange(favAdd: true)),
5782
5783                 ShortcutCommand.Create(Keys.Control | Keys.I)
5784                     .Do(() => this.DoRepliedStatusOpen()),
5785
5786                 ShortcutCommand.Create(Keys.Control | Keys.Q)
5787                     .Do(() => this.DoQuoteOfficial()),
5788
5789                 ShortcutCommand.Create(Keys.Control | Keys.B)
5790                     .Do(() => this.ReadedStripMenuItem_Click(this.ReadedStripMenuItem, EventArgs.Empty)),
5791
5792                 ShortcutCommand.Create(Keys.Control | Keys.T)
5793                     .Do(() => this.HashManageMenuItem_Click(this.HashManageMenuItem, EventArgs.Empty)),
5794
5795                 ShortcutCommand.Create(Keys.Control | Keys.L)
5796                     .Do(() => this.UrlConvertAutoToolStripMenuItem_Click(this.UrlConvertAutoToolStripMenuItem, EventArgs.Empty)),
5797
5798                 ShortcutCommand.Create(Keys.Control | Keys.Y)
5799                     .NotFocusedOn(FocusedControl.PostBrowser)
5800                     .Do(() => this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty)),
5801
5802                 ShortcutCommand.Create(Keys.Control | Keys.F)
5803                     .Do(() => this.MenuItemSubSearch_Click(this.MenuItemSubSearch, EventArgs.Empty)),
5804
5805                 ShortcutCommand.Create(Keys.Control | Keys.U)
5806                     .Do(() => this.ShowUserTimeline()),
5807
5808                 ShortcutCommand.Create(Keys.Control | Keys.H)
5809                     .Do(() => this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty)),
5810
5811                 ShortcutCommand.Create(Keys.Control | Keys.O)
5812                     .Do(() => this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty)),
5813
5814                 ShortcutCommand.Create(Keys.Control | Keys.E)
5815                     .Do(() => this.OpenURLMenuItem_Click(this.OpenURLMenuItem, EventArgs.Empty)),
5816
5817                 ShortcutCommand.Create(Keys.Control | Keys.Home, Keys.Control | Keys.End)
5818                     .FocusedOn(FocusedControl.ListTab)
5819                     .Do(() => this.selectionDebouncer.Call(), preventDefault: false),
5820
5821                 ShortcutCommand.Create(Keys.Control | Keys.N)
5822                     .FocusedOn(FocusedControl.ListTab)
5823                     .Do(() => this.GoNextTab(forward: true)),
5824
5825                 ShortcutCommand.Create(Keys.Control | Keys.P)
5826                     .FocusedOn(FocusedControl.ListTab)
5827                     .Do(() => this.GoNextTab(forward: false)),
5828
5829                 ShortcutCommand.Create(Keys.Control | Keys.C, Keys.Control | Keys.Insert)
5830                     .FocusedOn(FocusedControl.ListTab)
5831                     .Do(() => this.CopyStot()),
5832
5833                 // タブダイレクト選択(Ctrl+1~8,Ctrl+9)
5834                 ShortcutCommand.Create(Keys.Control | Keys.D1)
5835                     .FocusedOn(FocusedControl.ListTab)
5836                     .OnlyWhen(() => this.statuses.Tabs.Count >= 1)
5837                     .Do(() => this.ListTab.SelectedIndex = 0),
5838
5839                 ShortcutCommand.Create(Keys.Control | Keys.D2)
5840                     .FocusedOn(FocusedControl.ListTab)
5841                     .OnlyWhen(() => this.statuses.Tabs.Count >= 2)
5842                     .Do(() => this.ListTab.SelectedIndex = 1),
5843
5844                 ShortcutCommand.Create(Keys.Control | Keys.D3)
5845                     .FocusedOn(FocusedControl.ListTab)
5846                     .OnlyWhen(() => this.statuses.Tabs.Count >= 3)
5847                     .Do(() => this.ListTab.SelectedIndex = 2),
5848
5849                 ShortcutCommand.Create(Keys.Control | Keys.D4)
5850                     .FocusedOn(FocusedControl.ListTab)
5851                     .OnlyWhen(() => this.statuses.Tabs.Count >= 4)
5852                     .Do(() => this.ListTab.SelectedIndex = 3),
5853
5854                 ShortcutCommand.Create(Keys.Control | Keys.D5)
5855                     .FocusedOn(FocusedControl.ListTab)
5856                     .OnlyWhen(() => this.statuses.Tabs.Count >= 5)
5857                     .Do(() => this.ListTab.SelectedIndex = 4),
5858
5859                 ShortcutCommand.Create(Keys.Control | Keys.D6)
5860                     .FocusedOn(FocusedControl.ListTab)
5861                     .OnlyWhen(() => this.statuses.Tabs.Count >= 6)
5862                     .Do(() => this.ListTab.SelectedIndex = 5),
5863
5864                 ShortcutCommand.Create(Keys.Control | Keys.D7)
5865                     .FocusedOn(FocusedControl.ListTab)
5866                     .OnlyWhen(() => this.statuses.Tabs.Count >= 7)
5867                     .Do(() => this.ListTab.SelectedIndex = 6),
5868
5869                 ShortcutCommand.Create(Keys.Control | Keys.D8)
5870                     .FocusedOn(FocusedControl.ListTab)
5871                     .OnlyWhen(() => this.statuses.Tabs.Count >= 8)
5872                     .Do(() => this.ListTab.SelectedIndex = 7),
5873
5874                 ShortcutCommand.Create(Keys.Control | Keys.D9)
5875                     .FocusedOn(FocusedControl.ListTab)
5876                     .Do(() => this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1),
5877
5878                 ShortcutCommand.Create(Keys.Control | Keys.A)
5879                     .FocusedOn(FocusedControl.StatusText)
5880                     .Do(() => this.StatusText.SelectAll()),
5881
5882                 ShortcutCommand.Create(Keys.Control | Keys.V)
5883                     .FocusedOn(FocusedControl.StatusText)
5884                     .Do(() => this.ProcClipboardFromStatusTextWhenCtrlPlusV()),
5885
5886                 ShortcutCommand.Create(Keys.Control | Keys.Up)
5887                     .FocusedOn(FocusedControl.StatusText)
5888                     .Do(() => this.StatusTextHistoryBack()),
5889
5890                 ShortcutCommand.Create(Keys.Control | Keys.Down)
5891                     .FocusedOn(FocusedControl.StatusText)
5892                     .Do(() => this.StatusTextHistoryForward()),
5893
5894                 ShortcutCommand.Create(Keys.Control | Keys.PageUp, Keys.Control | Keys.P)
5895                     .FocusedOn(FocusedControl.StatusText)
5896                     .Do(() =>
5897                     {
5898                         if (this.ListTab.SelectedIndex == 0)
5899                         {
5900                             this.ListTab.SelectedIndex = this.ListTab.TabCount - 1;
5901                         }
5902                         else
5903                         {
5904                             this.ListTab.SelectedIndex -= 1;
5905                         }
5906                         this.StatusText.Focus();
5907                     }),
5908
5909                 ShortcutCommand.Create(Keys.Control | Keys.PageDown, Keys.Control | Keys.N)
5910                     .FocusedOn(FocusedControl.StatusText)
5911                     .Do(() =>
5912                     {
5913                         if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1)
5914                         {
5915                             this.ListTab.SelectedIndex = 0;
5916                         }
5917                         else
5918                         {
5919                             this.ListTab.SelectedIndex += 1;
5920                         }
5921                         this.StatusText.Focus();
5922                     }),
5923
5924                 ShortcutCommand.Create(Keys.Control | Keys.Y)
5925                     .FocusedOn(FocusedControl.PostBrowser)
5926                     .Do(() =>
5927                     {
5928                         var multiline = !SettingManager.Local.StatusMultiline;
5929                         SettingManager.Local.StatusMultiline = multiline;
5930                         this.MultiLineMenuItem.Checked = multiline;
5931                         this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty);
5932                     }),
5933
5934                 ShortcutCommand.Create(Keys.Shift | Keys.F3)
5935                     .Do(() => this.MenuItemSearchPrev_Click(this.MenuItemSearchPrev, EventArgs.Empty)),
5936
5937                 ShortcutCommand.Create(Keys.Shift | Keys.F5)
5938                     .Do(() => this.DoRefreshMore()),
5939
5940                 ShortcutCommand.Create(Keys.Shift | Keys.F6)
5941                     .Do(() => this.RefreshTabAsync<MentionsTabModel>(backward: true)),
5942
5943                 ShortcutCommand.Create(Keys.Shift | Keys.F7)
5944                     .Do(() => this.RefreshTabAsync<DirectMessagesTabModel>(backward: true)),
5945
5946                 ShortcutCommand.Create(Keys.Shift | Keys.R)
5947                     .NotFocusedOn(FocusedControl.StatusText)
5948                     .Do(() => this.DoRefreshMore()),
5949
5950                 ShortcutCommand.Create(Keys.Shift | Keys.H)
5951                     .FocusedOn(FocusedControl.ListTab)
5952                     .Do(() => this.GoTopEnd(goTop: true)),
5953
5954                 ShortcutCommand.Create(Keys.Shift | Keys.L)
5955                     .FocusedOn(FocusedControl.ListTab)
5956                     .Do(() => this.GoTopEnd(goTop: false)),
5957
5958                 ShortcutCommand.Create(Keys.Shift | Keys.M)
5959                     .FocusedOn(FocusedControl.ListTab)
5960                     .Do(() => this.GoMiddle()),
5961
5962                 ShortcutCommand.Create(Keys.Shift | Keys.G)
5963                     .FocusedOn(FocusedControl.ListTab)
5964                     .Do(() => this.GoLast()),
5965
5966                 ShortcutCommand.Create(Keys.Shift | Keys.Z)
5967                     .FocusedOn(FocusedControl.ListTab)
5968                     .Do(() => this.MoveMiddle()),
5969
5970                 ShortcutCommand.Create(Keys.Shift | Keys.Oem4)
5971                     .FocusedOn(FocusedControl.ListTab)
5972                     .Do(() => this.GoBackInReplyToPostTree(parallel: true, isForward: false)),
5973
5974                 ShortcutCommand.Create(Keys.Shift | Keys.Oem6)
5975                     .FocusedOn(FocusedControl.ListTab)
5976                     .Do(() => this.GoBackInReplyToPostTree(parallel: true, isForward: true)),
5977
5978                 // お気に入り前後ジャンプ(SHIFT+N←/P→)
5979                 ShortcutCommand.Create(Keys.Shift | Keys.Right, Keys.Shift | Keys.N)
5980                     .FocusedOn(FocusedControl.ListTab)
5981                     .Do(() => this.GoFav(forward: true)),
5982
5983                 // お気に入り前後ジャンプ(SHIFT+N←/P→)
5984                 ShortcutCommand.Create(Keys.Shift | Keys.Left, Keys.Shift | Keys.P)
5985                     .FocusedOn(FocusedControl.ListTab)
5986                     .Do(() => this.GoFav(forward: false)),
5987
5988                 ShortcutCommand.Create(Keys.Shift | Keys.Space)
5989                     .FocusedOn(FocusedControl.ListTab)
5990                     .Do(() => this.GoBackSelectPostChain()),
5991
5992                 ShortcutCommand.Create(Keys.Alt | Keys.R)
5993                     .Do(() => this.DoReTweetOfficial(isConfirm: true)),
5994
5995                 ShortcutCommand.Create(Keys.Alt | Keys.P)
5996                     .OnlyWhen(() => this.CurrentPost != null)
5997                     .Do(() => this.DoShowUserStatus(this.CurrentPost!.ScreenName, showInputDialog: false)),
5998
5999                 ShortcutCommand.Create(Keys.Alt | Keys.Up)
6000                     .Do(() => this.tweetDetailsView.ScrollDownPostBrowser(forward: false)),
6001
6002                 ShortcutCommand.Create(Keys.Alt | Keys.Down)
6003                     .Do(() => this.tweetDetailsView.ScrollDownPostBrowser(forward: true)),
6004
6005                 ShortcutCommand.Create(Keys.Alt | Keys.PageUp)
6006                     .Do(() => this.tweetDetailsView.PageDownPostBrowser(forward: false)),
6007
6008                 ShortcutCommand.Create(Keys.Alt | Keys.PageDown)
6009                     .Do(() => this.tweetDetailsView.PageDownPostBrowser(forward: true)),
6010
6011                 // 別タブの同じ書き込みへ(ALT+←/→)
6012                 ShortcutCommand.Create(Keys.Alt | Keys.Right)
6013                     .FocusedOn(FocusedControl.ListTab)
6014                     .Do(() => this.GoSamePostToAnotherTab(left: false)),
6015
6016                 ShortcutCommand.Create(Keys.Alt | Keys.Left)
6017                     .FocusedOn(FocusedControl.ListTab)
6018                     .Do(() => this.GoSamePostToAnotherTab(left: true)),
6019
6020                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.R)
6021                     .Do(() => this.MakeReplyText(atAll: true)),
6022
6023                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.C, Keys.Control | Keys.Shift | Keys.Insert)
6024                     .Do(() => this.CopyIdUri()),
6025
6026                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.F)
6027                     .OnlyWhen(() => this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
6028                     .Do(() => this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus()),
6029
6030                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.S)
6031                     .Do(() => this.FavoriteChange(favAdd: false)),
6032
6033                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.B)
6034                     .Do(() => this.UnreadStripMenuItem_Click(this.UnreadStripMenuItem, EventArgs.Empty)),
6035
6036                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.T)
6037                     .Do(() => this.HashToggleMenuItem_Click(this.HashToggleMenuItem, EventArgs.Empty)),
6038
6039                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.P)
6040                     .Do(() => this.ImageSelectMenuItem_Click(this.ImageSelectMenuItem, EventArgs.Empty)),
6041
6042                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.H)
6043                     .Do(() => this.DoMoveToRTHome()),
6044
6045                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Up)
6046                     .FocusedOn(FocusedControl.StatusText)
6047                     .Do(() =>
6048                     {
6049                         var tab = this.CurrentTab;
6050                         var selectedIndex = tab.SelectedIndex;
6051                         if (selectedIndex != -1 && selectedIndex > 0)
6052                         {
6053                             var listView = this.CurrentListView;
6054                             var idx = selectedIndex - 1;
6055                             this.SelectListItem(listView, idx);
6056                             listView.EnsureVisible(idx);
6057                         }
6058                     }),
6059
6060                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Down)
6061                     .FocusedOn(FocusedControl.StatusText)
6062                     .Do(() =>
6063                     {
6064                         var tab = this.CurrentTab;
6065                         var selectedIndex = tab.SelectedIndex;
6066                         if (selectedIndex != -1 && selectedIndex < tab.AllCount - 1)
6067                         {
6068                             var listView = this.CurrentListView;
6069                             var idx = selectedIndex + 1;
6070                             this.SelectListItem(listView, idx);
6071                             listView.EnsureVisible(idx);
6072                         }
6073                     }),
6074
6075                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Space)
6076                     .FocusedOn(FocusedControl.StatusText)
6077                     .Do(() =>
6078                     {
6079                         if (this.StatusText.SelectionStart > 0)
6080                         {
6081                             var endidx = this.StatusText.SelectionStart - 1;
6082                             var startstr = "";
6083                             for (var i = this.StatusText.SelectionStart - 1; i >= 0; i--)
6084                             {
6085                                 var c = this.StatusText.Text[i];
6086                                 if (char.IsLetterOrDigit(c) || c == '_')
6087                                 {
6088                                     continue;
6089                                 }
6090                                 if (c == '@')
6091                                 {
6092                                     startstr = this.StatusText.Text.Substring(i + 1, endidx - i);
6093                                     var cnt = this.AtIdSupl.ItemCount;
6094                                     this.ShowSuplDialog(this.StatusText, this.AtIdSupl, startstr.Length + 1, startstr);
6095                                     if (this.AtIdSupl.ItemCount != cnt)
6096                                         this.MarkSettingAtIdModified();
6097                                 }
6098                                 else if (c == '#')
6099                                 {
6100                                     startstr = this.StatusText.Text.Substring(i + 1, endidx - i);
6101                                     this.ShowSuplDialog(this.StatusText, this.HashSupl, startstr.Length + 1, startstr);
6102                                 }
6103                                 else
6104                                 {
6105                                     break;
6106                                 }
6107                             }
6108                         }
6109                     }),
6110
6111                 // ソートダイレクト選択(Ctrl+Shift+1~8,Ctrl+Shift+9)
6112                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D1)
6113                     .FocusedOn(FocusedControl.ListTab)
6114                     .Do(() => this.SetSortColumnByDisplayIndex(0)),
6115
6116                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D2)
6117                     .FocusedOn(FocusedControl.ListTab)
6118                     .Do(() => this.SetSortColumnByDisplayIndex(1)),
6119
6120                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D3)
6121                     .FocusedOn(FocusedControl.ListTab)
6122                     .Do(() => this.SetSortColumnByDisplayIndex(2)),
6123
6124                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D4)
6125                     .FocusedOn(FocusedControl.ListTab)
6126                     .Do(() => this.SetSortColumnByDisplayIndex(3)),
6127
6128                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D5)
6129                     .FocusedOn(FocusedControl.ListTab)
6130                     .Do(() => this.SetSortColumnByDisplayIndex(4)),
6131
6132                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D6)
6133                     .FocusedOn(FocusedControl.ListTab)
6134                     .Do(() => this.SetSortColumnByDisplayIndex(5)),
6135
6136                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D7)
6137                     .FocusedOn(FocusedControl.ListTab)
6138                     .Do(() => this.SetSortColumnByDisplayIndex(6)),
6139
6140                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D8)
6141                     .FocusedOn(FocusedControl.ListTab)
6142                     .Do(() => this.SetSortColumnByDisplayIndex(7)),
6143
6144                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D9)
6145                     .FocusedOn(FocusedControl.ListTab)
6146                     .Do(() => this.SetSortLastColumn()),
6147
6148                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.S)
6149                     .FocusedOn(FocusedControl.ListTab)
6150                     .Do(() => this.FavoritesRetweetOfficial()),
6151
6152                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.R)
6153                     .FocusedOn(FocusedControl.ListTab)
6154                     .Do(() => this.FavoritesRetweetUnofficial()),
6155
6156                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.H)
6157                     .FocusedOn(FocusedControl.ListTab)
6158                     .Do(() => this.OpenUserAppointUrl()),
6159
6160                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R)
6161                     .FocusedOn(FocusedControl.PostBrowser)
6162                     .Do(() => this.DoReTweetUnofficial()),
6163
6164                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.T)
6165                     .OnlyWhen(() => this.ExistCurrentPost)
6166                     .Do(() => this.tweetDetailsView.DoTranslation()),
6167
6168                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R)
6169                     .Do(() => this.DoReTweetUnofficial()),
6170
6171                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.C, Keys.Alt | Keys.Shift | Keys.Insert)
6172                     .Do(() => this.CopyUserId()),
6173
6174                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Up)
6175                     .Do(() => this.tweetThumbnail1.ScrollUp()),
6176
6177                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Down)
6178                     .Do(() => this.tweetThumbnail1.ScrollDown()),
6179
6180                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
6181                     .FocusedOn(FocusedControl.ListTab)
6182                     .OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
6183                     .Do(() => this.OpenThumbnailPicture(this.tweetThumbnail1.Thumbnail)),
6184             };
6185         }
6186
6187         internal bool CommonKeyDown(Keys keyData, FocusedControl focusedOn, out Task? asyncTask)
6188         {
6189             // Task を返す非同期処理があれば asyncTask に代入する
6190             asyncTask = null;
6191
6192             // ShortcutCommand に対応しているコマンドはここで処理される
6193             foreach (var command in this.shortcutCommands)
6194             {
6195                 if (command.IsMatch(keyData, focusedOn))
6196                 {
6197                     asyncTask = command.RunCommand();
6198                     return command.PreventDefault;
6199                 }
6200             }
6201
6202             return false;
6203         }
6204
6205         private void GoNextTab(bool forward)
6206         {
6207             var idx = this.statuses.SelectedTabIndex;
6208             var tabCount = this.statuses.Tabs.Count;
6209             if (forward)
6210             {
6211                 idx += 1;
6212                 if (idx > tabCount - 1) idx = 0;
6213             }
6214             else
6215             {
6216                 idx -= 1;
6217                 if (idx < 0) idx = tabCount - 1;
6218             }
6219             this.ListTab.SelectedIndex = idx;
6220         }
6221
6222         private void CopyStot()
6223         {
6224             var sb = new StringBuilder();
6225             var tab = this.CurrentTab;
6226             var isProtected = false;
6227             var isDm = tab.TabType == MyCommon.TabUsageType.DirectMessage;
6228             foreach (var post in tab.SelectedPosts)
6229             {
6230                 if (post.IsDeleted) continue;
6231                 if (!isDm)
6232                 {
6233                     if (post.RetweetedId != null)
6234                         sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.RetweetedId, Environment.NewLine);
6235                     else
6236                         sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine);
6237                 }
6238                 else
6239                 {
6240                     sb.AppendFormat("{0}:{1} [{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine);
6241                 }
6242             }
6243             if (isProtected)
6244             {
6245                 MessageBox.Show(Properties.Resources.CopyStotText1);
6246             }
6247             if (sb.Length > 0)
6248             {
6249                 var clstr = sb.ToString();
6250                 try
6251                 {
6252                     Clipboard.SetDataObject(clstr, false, 5, 100);
6253                 }
6254                 catch (Exception ex)
6255                 {
6256                     MessageBox.Show(ex.Message);
6257                 }
6258             }
6259         }
6260
6261         private void CopyIdUri()
6262         {
6263             var tab = this.CurrentTab;
6264             if (tab == null || tab is DirectMessagesTabModel)
6265                 return;
6266
6267             var copyUrls = new List<string>();
6268             foreach (var post in tab.SelectedPosts)
6269                 copyUrls.Add(MyCommon.GetStatusUrl(post));
6270
6271             if (copyUrls.Count == 0)
6272                 return;
6273
6274             try
6275             {
6276                 Clipboard.SetDataObject(string.Join(Environment.NewLine, copyUrls), false, 5, 100);
6277             }
6278             catch (ExternalException ex)
6279             {
6280                 MessageBox.Show(ex.Message);
6281             }
6282         }
6283
6284         private void GoFav(bool forward)
6285         {
6286             var tab = this.CurrentTab;
6287             if (tab.AllCount == 0)
6288                 return;
6289
6290             var selectedIndex = tab.SelectedIndex;
6291
6292             int fIdx;
6293             int toIdx;
6294             int stp;
6295
6296             if (forward)
6297             {
6298                 if (selectedIndex == -1)
6299                 {
6300                     fIdx = 0;
6301                 }
6302                 else
6303                 {
6304                     fIdx = selectedIndex + 1;
6305                     if (fIdx > tab.AllCount - 1)
6306                         return;
6307                 }
6308                 toIdx = tab.AllCount;
6309                 stp = 1;
6310             }
6311             else
6312             {
6313                 if (selectedIndex == -1)
6314                 {
6315                     fIdx = tab.AllCount - 1;
6316                 }
6317                 else
6318                 {
6319                     fIdx = selectedIndex - 1;
6320                     if (fIdx < 0)
6321                         return;
6322                 }
6323                 toIdx = -1;
6324                 stp = -1;
6325             }
6326
6327             for (var idx = fIdx; idx != toIdx; idx += stp)
6328             {
6329                 if (tab[idx].IsFav)
6330                 {
6331                     var listView = this.CurrentListView;
6332                     this.SelectListItem(listView, idx);
6333                     listView.EnsureVisible(idx);
6334                     break;
6335                 }
6336             }
6337         }
6338
6339         private void GoSamePostToAnotherTab(bool left)
6340         {
6341             var tab = this.CurrentTab;
6342
6343             // Directタブは対象外(見つかるはずがない)
6344             if (tab.TabType == MyCommon.TabUsageType.DirectMessage)
6345                 return;
6346
6347             var selectedStatusId = tab.SelectedStatusId;
6348             if (selectedStatusId == -1)
6349                 return;
6350
6351             int fIdx, toIdx, stp;
6352
6353             if (left)
6354             {
6355                 // 左のタブへ
6356                 if (this.ListTab.SelectedIndex == 0)
6357                 {
6358                     return;
6359                 }
6360                 else
6361                 {
6362                     fIdx = this.ListTab.SelectedIndex - 1;
6363                 }
6364                 toIdx = -1;
6365                 stp = -1;
6366             }
6367             else
6368             {
6369                 // 右のタブへ
6370                 if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1)
6371                 {
6372                     return;
6373                 }
6374                 else
6375                 {
6376                     fIdx = this.ListTab.SelectedIndex + 1;
6377                 }
6378                 toIdx = this.ListTab.TabCount;
6379                 stp = 1;
6380             }
6381
6382             for (var tabidx = fIdx; tabidx != toIdx; tabidx += stp)
6383             {
6384                 var targetTab = this.statuses.Tabs[tabidx];
6385
6386                 // Directタブは対象外
6387                 if (targetTab.TabType == MyCommon.TabUsageType.DirectMessage)
6388                     continue;
6389
6390                 var foundIndex = targetTab.IndexOf(selectedStatusId);
6391                 if (foundIndex != -1)
6392                 {
6393                     this.ListTab.SelectedIndex = tabidx;
6394                     var listView = this.CurrentListView;
6395                     this.SelectListItem(listView, foundIndex);
6396                     listView.EnsureVisible(foundIndex);
6397                     return;
6398                 }
6399             }
6400         }
6401
6402         private void GoPost(bool forward)
6403         {
6404             var tab = this.CurrentTab;
6405             var currentPost = this.CurrentPost;
6406
6407             if (currentPost == null)
6408                 return;
6409
6410             var selectedIndex = tab.SelectedIndex;
6411
6412             int fIdx, toIdx, stp;
6413
6414             if (forward)
6415             {
6416                 fIdx = selectedIndex + 1;
6417                 if (fIdx > tab.AllCount - 1) return;
6418                 toIdx = tab.AllCount;
6419                 stp = 1;
6420             }
6421             else
6422             {
6423                 fIdx = selectedIndex - 1;
6424                 if (fIdx < 0) return;
6425                 toIdx = -1;
6426                 stp = -1;
6427             }
6428
6429             string name;
6430             if (currentPost.RetweetedBy == null)
6431             {
6432                 name = currentPost.ScreenName;
6433             }
6434             else
6435             {
6436                 name = currentPost.RetweetedBy;
6437             }
6438             for (var idx = fIdx; idx != toIdx; idx += stp)
6439             {
6440                 var post = tab[idx];
6441                 if (post.RetweetedId == null)
6442                 {
6443                     if (post.ScreenName == name)
6444                     {
6445                         var listView = this.CurrentListView;
6446                         this.SelectListItem(listView, idx);
6447                         listView.EnsureVisible(idx);
6448                         break;
6449                     }
6450                 }
6451                 else
6452                 {
6453                     if (post.RetweetedBy == name)
6454                     {
6455                         var listView = this.CurrentListView;
6456                         this.SelectListItem(listView, idx);
6457                         listView.EnsureVisible(idx);
6458                         break;
6459                     }
6460                 }
6461             }
6462         }
6463
6464         private void GoRelPost(bool forward)
6465         {
6466             var tab = this.CurrentTab;
6467             var selectedIndex = tab.SelectedIndex;
6468
6469             if (selectedIndex == -1)
6470                 return;
6471
6472             int fIdx, toIdx, stp;
6473
6474             if (forward)
6475             {
6476                 fIdx = selectedIndex + 1;
6477                 if (fIdx > tab.AllCount - 1) return;
6478                 toIdx = tab.AllCount;
6479                 stp = 1;
6480             }
6481             else
6482             {
6483                 fIdx = selectedIndex - 1;
6484                 if (fIdx < 0) return;
6485                 toIdx = -1;
6486                 stp = -1;
6487             }
6488
6489             if (!this.anchorFlag)
6490             {
6491                 var currentPost = this.CurrentPost;
6492                 if (currentPost == null) return;
6493                 this.anchorPost = currentPost;
6494                 this.anchorFlag = true;
6495             }
6496             else
6497             {
6498                 if (this.anchorPost == null) return;
6499             }
6500
6501             for (var idx = fIdx; idx != toIdx; idx += stp)
6502             {
6503                 var post = tab[idx];
6504                 if (post.ScreenName == this.anchorPost.ScreenName ||
6505                     post.RetweetedBy == this.anchorPost.ScreenName ||
6506                     post.ScreenName == this.anchorPost.RetweetedBy ||
6507                     (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == this.anchorPost.RetweetedBy) ||
6508                     this.anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) ||
6509                     this.anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) ||
6510                     post.ReplyToList.Any(x => x.UserId == this.anchorPost.UserId) ||
6511                     post.ReplyToList.Any(x => x.UserId == this.anchorPost.RetweetedByUserId))
6512                 {
6513                     var listView = this.CurrentListView;
6514                     this.SelectListItem(listView, idx);
6515                     listView.EnsureVisible(idx);
6516                     break;
6517                 }
6518             }
6519         }
6520
6521         private void GoAnchor()
6522         {
6523             if (this.anchorPost == null) return;
6524             var idx = this.CurrentTab.IndexOf(this.anchorPost.StatusId);
6525             if (idx == -1) return;
6526
6527             var listView = this.CurrentListView;
6528             this.SelectListItem(listView, idx);
6529             listView.EnsureVisible(idx);
6530         }
6531
6532         private void GoTopEnd(bool goTop)
6533         {
6534             var listView = this.CurrentListView;
6535             if (listView.VirtualListSize == 0)
6536                 return;
6537
6538             ListViewItem item;
6539             int idx;
6540
6541             if (goTop)
6542             {
6543                 item = listView.GetItemAt(0, 25);
6544                 if (item == null)
6545                     idx = 0;
6546                 else
6547                     idx = item.Index;
6548             }
6549             else
6550             {
6551                 item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
6552                 if (item == null)
6553                     idx = listView.VirtualListSize - 1;
6554                 else
6555                     idx = item.Index;
6556             }
6557             this.SelectListItem(listView, idx);
6558         }
6559
6560         private void GoMiddle()
6561         {
6562             var listView = this.CurrentListView;
6563             if (listView.VirtualListSize == 0)
6564                 return;
6565
6566             ListViewItem item;
6567             int idx1;
6568             int idx2;
6569             int idx3;
6570
6571             item = listView.GetItemAt(0, 0);
6572             if (item == null)
6573             {
6574                 idx1 = 0;
6575             }
6576             else
6577             {
6578                 idx1 = item.Index;
6579             }
6580
6581             item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
6582             if (item == null)
6583             {
6584                 idx2 = listView.VirtualListSize - 1;
6585             }
6586             else
6587             {
6588                 idx2 = item.Index;
6589             }
6590             idx3 = (idx1 + idx2) / 2;
6591
6592             this.SelectListItem(listView, idx3);
6593         }
6594
6595         private void GoLast()
6596         {
6597             var listView = this.CurrentListView;
6598             if (listView.VirtualListSize == 0) return;
6599
6600             if (this.statuses.SortOrder == SortOrder.Ascending)
6601             {
6602                 this.SelectListItem(listView, listView.VirtualListSize - 1);
6603                 listView.EnsureVisible(listView.VirtualListSize - 1);
6604             }
6605             else
6606             {
6607                 this.SelectListItem(listView, 0);
6608                 listView.EnsureVisible(0);
6609             }
6610         }
6611
6612         private void MoveTop()
6613         {
6614             var listView = this.CurrentListView;
6615             if (listView.SelectedIndices.Count == 0) return;
6616             var idx = listView.SelectedIndices[0];
6617             if (this.statuses.SortOrder == SortOrder.Ascending)
6618             {
6619                 listView.EnsureVisible(listView.VirtualListSize - 1);
6620             }
6621             else
6622             {
6623                 listView.EnsureVisible(0);
6624             }
6625             listView.EnsureVisible(idx);
6626         }
6627
6628         private async Task GoInReplyToPostTree()
6629         {
6630             var curTabClass = this.CurrentTab;
6631             var currentPost = this.CurrentPost;
6632
6633             if (currentPost == null)
6634                 return;
6635
6636             if (curTabClass.TabType == MyCommon.TabUsageType.PublicSearch && currentPost.InReplyToStatusId == null && currentPost.TextFromApi.Contains("@"))
6637             {
6638                 try
6639                 {
6640                     var post = await this.tw.GetStatusApi(false, currentPost.StatusId);
6641
6642                     currentPost.InReplyToStatusId = post.InReplyToStatusId;
6643                     currentPost.InReplyToUser = post.InReplyToUser;
6644                     currentPost.IsReply = post.IsReply;
6645                     this.PurgeListViewItemCache();
6646
6647                     var index = curTabClass.SelectedIndex;
6648                     this.CurrentListView.RedrawItems(index, index, false);
6649                 }
6650                 catch (WebApiException ex)
6651                 {
6652                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
6653                 }
6654             }
6655
6656             if (!(this.ExistCurrentPost && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)) return;
6657
6658             if (this.replyChains == null || (this.replyChains.Count > 0 && this.replyChains.Peek().InReplyToId != currentPost.StatusId))
6659             {
6660                 this.replyChains = new Stack<ReplyChain>();
6661             }
6662             this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass));
6663
6664             int inReplyToIndex;
6665             string inReplyToTabName;
6666             var inReplyToId = currentPost.InReplyToStatusId.Value;
6667             var inReplyToUser = currentPost.InReplyToUser;
6668
6669             var inReplyToPosts = from tab in this.statuses.Tabs
6670                                  orderby tab != curTabClass
6671                                  from post in tab.Posts.Values
6672                                  where post.StatusId == inReplyToId
6673                                  let index = tab.IndexOf(post.StatusId)
6674                                  where index != -1
6675                                  select new { Tab = tab, Index = index };
6676
6677             var inReplyPost = inReplyToPosts.FirstOrDefault();
6678             if (inReplyPost == null)
6679             {
6680                 try
6681                 {
6682                     await Task.Run(async () =>
6683                     {
6684                         var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value)
6685                             .ConfigureAwait(false);
6686                         post.IsRead = true;
6687
6688                         this.statuses.AddPost(post);
6689                         this.statuses.DistributePosts();
6690                     });
6691                 }
6692                 catch (WebApiException ex)
6693                 {
6694                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
6695                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
6696                     return;
6697                 }
6698
6699                 this.RefreshTimeline();
6700
6701                 inReplyPost = inReplyToPosts.FirstOrDefault();
6702                 if (inReplyPost == null)
6703                 {
6704                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
6705                     return;
6706                 }
6707             }
6708             inReplyToTabName = inReplyPost.Tab.TabName;
6709             inReplyToIndex = inReplyPost.Index;
6710
6711             var tabIndex = this.statuses.Tabs.IndexOf(inReplyToTabName);
6712             var tabPage = this.ListTab.TabPages[tabIndex];
6713             var listView = (DetailsListView)tabPage.Tag;
6714
6715             if (this.CurrentTabName != inReplyToTabName)
6716             {
6717                 this.ListTab.SelectedIndex = tabIndex;
6718             }
6719
6720             this.SelectListItem(listView, inReplyToIndex);
6721             listView.EnsureVisible(inReplyToIndex);
6722         }
6723
6724         private void GoBackInReplyToPostTree(bool parallel = false, bool isForward = true)
6725         {
6726             var curTabClass = this.CurrentTab;
6727             var currentPost = this.CurrentPost;
6728
6729             if (currentPost == null)
6730                 return;
6731
6732             if (parallel)
6733             {
6734                 if (currentPost.InReplyToStatusId != null)
6735                 {
6736                     var posts = from t in this.statuses.Tabs
6737                                 from p in t.Posts
6738                                 where p.Value.StatusId != currentPost.StatusId && p.Value.InReplyToStatusId == currentPost.InReplyToStatusId
6739                                 let indexOf = t.IndexOf(p.Value.StatusId)
6740                                 where indexOf > -1
6741                                 orderby isForward ? indexOf : indexOf * -1
6742                                 orderby t != curTabClass
6743                                 select new { Tab = t, Post = p.Value, Index = indexOf };
6744                     try
6745                     {
6746                         var postList = posts.ToList();
6747                         for (var i = postList.Count - 1; i >= 0; i--)
6748                         {
6749                             var index = i;
6750                             if (postList.FindIndex(pst => pst.Post.StatusId == postList[index].Post.StatusId) != index)
6751                             {
6752                                 postList.RemoveAt(index);
6753                             }
6754                         }
6755                         var currentIndex = this.CurrentTab.SelectedIndex;
6756                         var post = postList.FirstOrDefault(pst => pst.Tab == curTabClass && isForward ? pst.Index > currentIndex : pst.Index < currentIndex);
6757                         if (post == null) post = postList.FirstOrDefault(pst => pst.Tab != curTabClass);
6758                         if (post == null) post = postList.First();
6759                         var tabIndex = this.statuses.Tabs.IndexOf(post.Tab);
6760                         this.ListTab.SelectedIndex = tabIndex;
6761                         var listView = this.CurrentListView;
6762                         this.SelectListItem(listView, post.Index);
6763                         listView.EnsureVisible(post.Index);
6764                     }
6765                     catch (InvalidOperationException)
6766                     {
6767                         return;
6768                     }
6769                 }
6770             }
6771             else
6772             {
6773                 if (this.replyChains == null || this.replyChains.Count < 1)
6774                 {
6775                     var posts = from t in this.statuses.Tabs
6776                                 from p in t.Posts
6777                                 where p.Value.InReplyToStatusId == currentPost.StatusId
6778                                 let indexOf = t.IndexOf(p.Value.StatusId)
6779                                 where indexOf > -1
6780                                 orderby indexOf
6781                                 orderby t != curTabClass
6782                                 select new { Tab = t, Index = indexOf };
6783                     try
6784                     {
6785                         var post = posts.First();
6786                         var tabIndex = this.statuses.Tabs.IndexOf(post.Tab);
6787                         this.ListTab.SelectedIndex = tabIndex;
6788                         var listView = this.CurrentListView;
6789                         this.SelectListItem(listView, post.Index);
6790                         listView.EnsureVisible(post.Index);
6791                     }
6792                     catch (InvalidOperationException)
6793                     {
6794                         return;
6795                     }
6796                 }
6797                 else
6798                 {
6799                     var chainHead = this.replyChains.Pop();
6800                     if (chainHead.InReplyToId == currentPost.StatusId)
6801                     {
6802                         var tab = chainHead.OriginalTab;
6803                         if (!this.statuses.Tabs.Contains(tab))
6804                         {
6805                             this.replyChains = null;
6806                         }
6807                         else
6808                         {
6809                             var idx = tab.IndexOf(chainHead.OriginalId);
6810                             if (idx == -1)
6811                             {
6812                                 this.replyChains = null;
6813                             }
6814                             else
6815                             {
6816                                 var tabIndex = this.statuses.Tabs.IndexOf(tab);
6817                                 try
6818                                 {
6819                                     this.ListTab.SelectedIndex = tabIndex;
6820                                 }
6821                                 catch (Exception)
6822                                 {
6823                                     this.replyChains = null;
6824                                 }
6825                                 var listView = this.CurrentListView;
6826                                 this.SelectListItem(listView, idx);
6827                                 listView.EnsureVisible(idx);
6828                             }
6829                         }
6830                     }
6831                     else
6832                     {
6833                         this.replyChains = null;
6834                         this.GoBackInReplyToPostTree(parallel);
6835                     }
6836                 }
6837             }
6838         }
6839
6840         private void GoBackSelectPostChain()
6841         {
6842             if (this.selectPostChains.Count > 1)
6843             {
6844                 var idx = -1;
6845                 TabModel? foundTab = null;
6846
6847                 do
6848                 {
6849                     try
6850                     {
6851                         this.selectPostChains.Pop();
6852                         var (tab, post) = this.selectPostChains.Peek();
6853
6854                         if (!this.statuses.Tabs.Contains(tab))
6855                             continue; // 該当タブが存在しないので無視
6856
6857                         if (post != null)
6858                         {
6859                             idx = tab.IndexOf(post.StatusId);
6860                             if (idx == -1) continue;  // 該当ポストが存在しないので無視
6861                         }
6862
6863                         foundTab = tab;
6864
6865                         this.selectPostChains.Pop();
6866                     }
6867                     catch (InvalidOperationException)
6868                     {
6869                     }
6870
6871                     break;
6872                 }
6873                 while (this.selectPostChains.Count > 1);
6874
6875                 if (foundTab == null)
6876                 {
6877                     // 状態がおかしいので処理を中断
6878                     // 履歴が残り1つであればクリアしておく
6879                     if (this.selectPostChains.Count == 1)
6880                         this.selectPostChains.Clear();
6881                     return;
6882                 }
6883
6884                 var tabIndex = this.statuses.Tabs.IndexOf(foundTab);
6885                 var tabPage = this.ListTab.TabPages[tabIndex];
6886                 var lst = (DetailsListView)tabPage.Tag;
6887                 this.ListTab.SelectedIndex = tabIndex;
6888
6889                 if (idx > -1)
6890                 {
6891                     this.SelectListItem(lst, idx);
6892                     lst.EnsureVisible(idx);
6893                 }
6894                 lst.Focus();
6895             }
6896         }
6897
6898         private void PushSelectPostChain()
6899         {
6900             var currentTab = this.CurrentTab;
6901             var currentPost = this.CurrentPost;
6902
6903             var count = this.selectPostChains.Count;
6904             if (count > 0)
6905             {
6906                 var (tab, post) = this.selectPostChains.Peek();
6907                 if (tab == currentTab)
6908                 {
6909                     if (post == currentPost) return;  // 最新の履歴と同一
6910                     if (post == null) this.selectPostChains.Pop();  // 置き換えるため削除
6911                 }
6912             }
6913             if (count >= 2500) this.TrimPostChain();
6914             this.selectPostChains.Push((currentTab, currentPost));
6915         }
6916
6917         private void TrimPostChain()
6918         {
6919             if (this.selectPostChains.Count <= 2000) return;
6920             var p = new Stack<(TabModel, PostClass?)>(2000);
6921             for (var i = 0; i < 2000; i++)
6922             {
6923                 p.Push(this.selectPostChains.Pop());
6924             }
6925             this.selectPostChains.Clear();
6926             for (var i = 0; i < 2000; i++)
6927             {
6928                 this.selectPostChains.Push(p.Pop());
6929             }
6930         }
6931
6932         private bool GoStatus(long statusId)
6933         {
6934             if (statusId == 0) return false;
6935
6936             var tab = this.statuses.Tabs
6937                 .Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage)
6938                 .Where(x => x.Contains(statusId))
6939                 .FirstOrDefault();
6940
6941             if (tab == null)
6942                 return false;
6943
6944             var index = tab.IndexOf(statusId);
6945
6946             var tabIndex = this.statuses.Tabs.IndexOf(tab);
6947             this.ListTab.SelectedIndex = tabIndex;
6948
6949             var listView = this.CurrentListView;
6950             this.SelectListItem(listView, index);
6951             listView.EnsureVisible(index);
6952
6953             return true;
6954         }
6955
6956         private bool GoDirectMessage(long statusId)
6957         {
6958             if (statusId == 0) return false;
6959
6960             var tab = this.statuses.DirectMessageTab;
6961             var index = tab.IndexOf(statusId);
6962
6963             if (index == -1)
6964                 return false;
6965
6966             var tabIndex = this.statuses.Tabs.IndexOf(tab);
6967             this.ListTab.SelectedIndex = tabIndex;
6968
6969             var listView = this.CurrentListView;
6970             this.SelectListItem(listView, index);
6971             listView.EnsureVisible(index);
6972
6973             return true;
6974         }
6975
6976         private void MyList_MouseClick(object sender, MouseEventArgs e)
6977             => this.anchorFlag = false;
6978
6979         private void StatusText_Enter(object sender, EventArgs e)
6980         {
6981             // フォーカスの戻り先を StatusText に設定
6982             this.Tag = this.StatusText;
6983             this.StatusText.BackColor = this.clInputBackcolor;
6984         }
6985
6986         public Color InputBackColor
6987         {
6988             get => this.clInputBackcolor;
6989             set => this.clInputBackcolor = value;
6990         }
6991
6992         private void StatusText_Leave(object sender, EventArgs e)
6993         {
6994             // フォーカスがメニューに遷移しないならばフォーカスはタブに移ることを期待
6995             if (this.ListTab.SelectedTab != null && this.MenuStrip1.Tag == null) this.Tag = this.ListTab.SelectedTab.Tag;
6996             this.StatusText.BackColor = Color.FromKnownColor(KnownColor.Window);
6997         }
6998
6999         private async void StatusText_KeyDown(object sender, KeyEventArgs e)
7000         {
7001             if (this.CommonKeyDown(e.KeyData, FocusedControl.StatusText, out var asyncTask))
7002             {
7003                 e.Handled = true;
7004                 e.SuppressKeyPress = true;
7005             }
7006
7007             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
7008
7009             if (asyncTask != null)
7010                 await asyncTask;
7011         }
7012
7013         private void SaveConfigsAll(bool ifModified)
7014         {
7015             if (!ifModified)
7016             {
7017                 this.SaveConfigsCommon();
7018                 this.SaveConfigsLocal();
7019                 this.SaveConfigsTabs();
7020                 this.SaveConfigsAtId();
7021             }
7022             else
7023             {
7024                 if (this.ModifySettingCommon) this.SaveConfigsCommon();
7025                 if (this.ModifySettingLocal) this.SaveConfigsLocal();
7026                 if (this.ModifySettingAtId) this.SaveConfigsAtId();
7027             }
7028         }
7029
7030         private void SaveConfigsAtId()
7031         {
7032             if (this.ignoreConfigSave || !SettingManager.Common.UseAtIdSupplement && this.AtIdSupl == null) return;
7033
7034             this.ModifySettingAtId = false;
7035             SettingManager.AtIdList.AtIdList = this.AtIdSupl.GetItemList();
7036             SettingManager.SaveAtIdList();
7037         }
7038
7039         private void SaveConfigsCommon()
7040         {
7041             if (this.ignoreConfigSave) return;
7042
7043             this.ModifySettingCommon = false;
7044             lock (this.syncObject)
7045             {
7046                 SettingManager.Common.UserName = this.tw.Username;
7047                 SettingManager.Common.UserId = this.tw.UserId;
7048                 SettingManager.Common.Token = this.tw.AccessToken;
7049                 SettingManager.Common.TokenSecret = this.tw.AccessTokenSecret;
7050                 SettingManager.Common.SortOrder = (int)this.statuses.SortOrder;
7051                 SettingManager.Common.SortColumn = this.statuses.SortMode switch
7052                 {
7053                     ComparerMode.Nickname => 1, // ニックネーム
7054                     ComparerMode.Data => 2, // 本文
7055                     ComparerMode.Id => 3, // 時刻=発言Id
7056                     ComparerMode.Name => 4, // 名前
7057                     ComparerMode.Source => 7, // Source
7058                     _ => throw new InvalidOperationException($"Invalid sort mode: {this.statuses.SortMode}"),
7059                 };
7060                 SettingManager.Common.HashTags = this.HashMgr.HashHistories;
7061                 if (this.HashMgr.IsPermanent)
7062                 {
7063                     SettingManager.Common.HashSelected = this.HashMgr.UseHash;
7064                 }
7065                 else
7066                 {
7067                     SettingManager.Common.HashSelected = "";
7068                 }
7069                 SettingManager.Common.HashIsHead = this.HashMgr.IsHead;
7070                 SettingManager.Common.HashIsPermanent = this.HashMgr.IsPermanent;
7071                 SettingManager.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
7072                 SettingManager.Common.UseImageService = this.ImageSelector.ServiceIndex;
7073                 SettingManager.Common.UseImageServiceName = this.ImageSelector.ServiceName;
7074
7075                 SettingManager.SaveCommon();
7076             }
7077         }
7078
7079         private void SaveConfigsLocal()
7080         {
7081             if (this.ignoreConfigSave) return;
7082             lock (this.syncObject)
7083             {
7084                 this.ModifySettingLocal = false;
7085                 SettingManager.Local.ScaleDimension = this.CurrentAutoScaleDimensions;
7086                 SettingManager.Local.FormSize = this.mySize;
7087                 SettingManager.Local.FormLocation = this.myLoc;
7088                 SettingManager.Local.SplitterDistance = this.mySpDis;
7089                 SettingManager.Local.PreviewDistance = this.mySpDis3;
7090                 SettingManager.Local.StatusMultiline = this.StatusText.Multiline;
7091                 SettingManager.Local.StatusTextHeight = this.mySpDis2;
7092
7093                 SettingManager.Local.FontUnread = this.fntUnread;
7094                 SettingManager.Local.ColorUnread = this.clUnread;
7095                 SettingManager.Local.FontRead = this.fntReaded;
7096                 SettingManager.Local.ColorRead = this.clReaded;
7097                 SettingManager.Local.FontDetail = this.fntDetail;
7098                 SettingManager.Local.ColorDetail = this.clDetail;
7099                 SettingManager.Local.ColorDetailBackcolor = this.clDetailBackcolor;
7100                 SettingManager.Local.ColorDetailLink = this.clDetailLink;
7101                 SettingManager.Local.ColorFav = this.clFav;
7102                 SettingManager.Local.ColorOWL = this.clOWL;
7103                 SettingManager.Local.ColorRetweet = this.clRetweet;
7104                 SettingManager.Local.ColorSelf = this.clSelf;
7105                 SettingManager.Local.ColorAtSelf = this.clAtSelf;
7106                 SettingManager.Local.ColorTarget = this.clTarget;
7107                 SettingManager.Local.ColorAtTarget = this.clAtTarget;
7108                 SettingManager.Local.ColorAtFromTarget = this.clAtFromTarget;
7109                 SettingManager.Local.ColorAtTo = this.clAtTo;
7110                 SettingManager.Local.ColorListBackcolor = this.clListBackcolor;
7111                 SettingManager.Local.ColorInputBackcolor = this.clInputBackcolor;
7112                 SettingManager.Local.ColorInputFont = this.clInputFont;
7113                 SettingManager.Local.FontInputFont = this.fntInputFont;
7114
7115                 if (this.ignoreConfigSave) return;
7116                 SettingManager.SaveLocal();
7117             }
7118         }
7119
7120         private void SaveConfigsTabs()
7121         {
7122             var tabSettingList = new List<SettingTabs.SettingTabItem>();
7123
7124             var tabs = this.statuses.Tabs.Append(this.statuses.MuteTab);
7125
7126             foreach (var tab in tabs)
7127             {
7128                 if (!tab.IsPermanentTabType)
7129                     continue;
7130
7131                 var tabSetting = new SettingTabs.SettingTabItem
7132                 {
7133                     TabName = tab.TabName,
7134                     TabType = tab.TabType,
7135                     UnreadManage = tab.UnreadManage,
7136                     Protected = tab.Protected,
7137                     Notify = tab.Notify,
7138                     SoundFile = tab.SoundFile,
7139                 };
7140
7141                 switch (tab)
7142                 {
7143                     case FilterTabModel filterTab:
7144                         tabSetting.FilterArray = filterTab.FilterArray;
7145                         break;
7146                     case UserTimelineTabModel userTab:
7147                         tabSetting.User = userTab.ScreenName;
7148                         break;
7149                     case PublicSearchTabModel searchTab:
7150                         tabSetting.SearchWords = searchTab.SearchWords;
7151                         tabSetting.SearchLang = searchTab.SearchLang;
7152                         break;
7153                     case ListTimelineTabModel listTab:
7154                         tabSetting.ListInfo = listTab.ListInfo;
7155                         break;
7156                 }
7157
7158                 tabSettingList.Add(tabSetting);
7159             }
7160
7161             SettingManager.Tabs.Tabs = tabSettingList;
7162             SettingManager.SaveTabs();
7163         }
7164
7165         private async void OpenURLFileMenuItem_Click(object sender, EventArgs e)
7166         {
7167             var ret = InputDialog.Show(this, Properties.Resources.OpenURL_InputText, Properties.Resources.OpenURL_Caption, out var inputText);
7168             if (ret != DialogResult.OK)
7169                 return;
7170
7171             var match = Twitter.StatusUrlRegex.Match(inputText);
7172             if (!match.Success)
7173             {
7174                 MessageBox.Show(
7175                     this,
7176                     Properties.Resources.OpenURL_InvalidFormat,
7177                     Properties.Resources.OpenURL_Caption,
7178                     MessageBoxButtons.OK,
7179                     MessageBoxIcon.Error);
7180                 return;
7181             }
7182
7183             try
7184             {
7185                 var statusId = long.Parse(match.Groups["StatusId"].Value);
7186                 await this.OpenRelatedTab(statusId);
7187             }
7188             catch (TabException ex)
7189             {
7190                 MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
7191             }
7192         }
7193
7194         private void SaveLogMenuItem_Click(object sender, EventArgs e)
7195         {
7196             var tab = this.CurrentTab;
7197
7198             var rslt = MessageBox.Show(
7199                 string.Format(Properties.Resources.SaveLogMenuItem_ClickText1, Environment.NewLine),
7200                 Properties.Resources.SaveLogMenuItem_ClickText2,
7201                 MessageBoxButtons.YesNoCancel,
7202                 MessageBoxIcon.Question);
7203             if (rslt == DialogResult.Cancel) return;
7204
7205             this.SaveFileDialog1.FileName = $"{ApplicationSettings.AssemblyName}Posts{DateTimeUtc.Now.ToLocalTime():yyMMdd-HHmmss}.tsv";
7206             this.SaveFileDialog1.InitialDirectory = Application.ExecutablePath;
7207             this.SaveFileDialog1.Filter = Properties.Resources.SaveLogMenuItem_ClickText3;
7208             this.SaveFileDialog1.FilterIndex = 0;
7209             this.SaveFileDialog1.Title = Properties.Resources.SaveLogMenuItem_ClickText4;
7210             this.SaveFileDialog1.RestoreDirectory = true;
7211
7212             if (this.SaveFileDialog1.ShowDialog() == DialogResult.OK)
7213             {
7214                 if (!this.SaveFileDialog1.ValidateNames) return;
7215                 using var sw = new StreamWriter(this.SaveFileDialog1.FileName, false, Encoding.UTF8);
7216                 if (rslt == DialogResult.Yes)
7217                 {
7218                     // All
7219                     for (var idx = 0; idx < tab.AllCount; idx++)
7220                     {
7221                         var post = tab[idx];
7222                         var protect = "";
7223                         if (post.IsProtect)
7224                             protect = "Protect";
7225                         sw.WriteLine(post.Nickname + "\t" +
7226                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
7227                                  post.CreatedAt.ToLocalTimeString() + "\t" +
7228                                  post.ScreenName + "\t" +
7229                                  post.StatusId + "\t" +
7230                                  post.ImageUrl + "\t" +
7231                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
7232                                  protect);
7233                     }
7234                 }
7235                 else
7236                 {
7237                     foreach (var post in this.CurrentTab.SelectedPosts)
7238                     {
7239                         var protect = "";
7240                         if (post.IsProtect)
7241                             protect = "Protect";
7242                         sw.WriteLine(post.Nickname + "\t" +
7243                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
7244                                  post.CreatedAt.ToLocalTimeString() + "\t" +
7245                                  post.ScreenName + "\t" +
7246                                  post.StatusId + "\t" +
7247                                  post.ImageUrl + "\t" +
7248                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
7249                                  protect);
7250                     }
7251                 }
7252             }
7253             this.TopMost = SettingManager.Common.AlwaysTop;
7254         }
7255
7256         public bool TabRename(string origTabName, [NotNullWhen(true)] out string? newTabName)
7257         {
7258             // タブ名変更
7259             newTabName = null;
7260             using (var inputName = new InputTabName())
7261             {
7262                 inputName.TabName = origTabName;
7263                 inputName.ShowDialog();
7264                 if (inputName.DialogResult == DialogResult.Cancel) return false;
7265                 newTabName = inputName.TabName;
7266             }
7267             this.TopMost = SettingManager.Common.AlwaysTop;
7268             if (!MyCommon.IsNullOrEmpty(newTabName))
7269             {
7270                 // 新タブ名存在チェック
7271                 if (this.statuses.ContainsTab(newTabName))
7272                 {
7273                     var tmp = string.Format(Properties.Resources.Tabs_DoubleClickText1, newTabName);
7274                     MessageBox.Show(tmp, Properties.Resources.Tabs_DoubleClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
7275                     return false;
7276                 }
7277
7278                 var tabIndex = this.statuses.Tabs.IndexOf(origTabName);
7279                 var tabPage = this.ListTab.TabPages[tabIndex];
7280
7281                 // タブ名を変更
7282                 if (tabPage != null)
7283                     tabPage.Text = newTabName;
7284
7285                 this.statuses.RenameTab(origTabName, newTabName);
7286
7287                 this.SaveConfigsCommon();
7288                 this.SaveConfigsTabs();
7289                 this.rclickTabName = newTabName;
7290                 return true;
7291             }
7292             else
7293             {
7294                 return false;
7295             }
7296         }
7297
7298         private void ListTab_MouseClick(object sender, MouseEventArgs e)
7299         {
7300             if (e.Button == MouseButtons.Middle)
7301             {
7302                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
7303                 {
7304                     if (this.ListTab.GetTabRect(index).Contains(e.Location))
7305                     {
7306                         this.RemoveSpecifiedTab(tab.TabName, true);
7307                         this.SaveConfigsTabs();
7308                         break;
7309                     }
7310                 }
7311             }
7312         }
7313
7314         private void ListTab_DoubleClick(object sender, MouseEventArgs e)
7315             => this.TabRename(this.CurrentTabName, out _);
7316
7317         private void ListTab_MouseDown(object sender, MouseEventArgs e)
7318         {
7319             if (SettingManager.Common.TabMouseLock) return;
7320             if (e.Button == MouseButtons.Left)
7321             {
7322                 foreach (var i in Enumerable.Range(0, this.statuses.Tabs.Count))
7323                 {
7324                     if (this.ListTab.GetTabRect(i).Contains(e.Location))
7325                     {
7326                         this.tabDrag = true;
7327                         this.tabMouseDownPoint = e.Location;
7328                         break;
7329                     }
7330                 }
7331             }
7332             else
7333             {
7334                 this.tabDrag = false;
7335             }
7336         }
7337
7338         private void ListTab_DragEnter(object sender, DragEventArgs e)
7339         {
7340             if (e.Data.GetDataPresent(typeof(TabPage)))
7341                 e.Effect = DragDropEffects.Move;
7342             else
7343                 e.Effect = DragDropEffects.None;
7344         }
7345
7346         private void ListTab_DragDrop(object sender, DragEventArgs e)
7347         {
7348             if (!e.Data.GetDataPresent(typeof(TabPage))) return;
7349
7350             this.tabDrag = false;
7351             var tn = "";
7352             var bef = false;
7353             var cpos = new Point(e.X, e.Y);
7354             var spos = this.ListTab.PointToClient(cpos);
7355             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
7356             {
7357                 var rect = this.ListTab.GetTabRect(index);
7358                 if (rect.Contains(spos))
7359                 {
7360                     tn = tab.TabName;
7361                     if (spos.X <= (rect.Left + rect.Right) / 2)
7362                         bef = true;
7363                     else
7364                         bef = false;
7365
7366                     break;
7367                 }
7368             }
7369
7370             // タブのないところにドロップ->最後尾へ移動
7371             if (MyCommon.IsNullOrEmpty(tn))
7372             {
7373                 var lastTab = this.statuses.Tabs.Last();
7374                 tn = lastTab.TabName;
7375                 bef = false;
7376             }
7377
7378             var tp = (TabPage)e.Data.GetData(typeof(TabPage));
7379             if (tp.Text == tn) return;
7380
7381             this.ReOrderTab(tp.Text, tn, bef);
7382         }
7383
7384         public void ReOrderTab(string targetTabText, string baseTabText, bool isBeforeBaseTab)
7385         {
7386             var baseIndex = this.GetTabPageIndex(baseTabText);
7387             if (baseIndex == -1)
7388                 return;
7389
7390             var targetIndex = this.GetTabPageIndex(targetTabText);
7391             if (targetIndex == -1)
7392                 return;
7393
7394             using (ControlTransaction.Layout(this.ListTab))
7395             {
7396                 var tab = this.statuses.Tabs[targetIndex];
7397                 var tabPage = this.ListTab.TabPages[targetIndex];
7398
7399                 this.ListTab.TabPages.Remove(tabPage);
7400
7401                 if (targetIndex < baseIndex)
7402                     baseIndex--;
7403
7404                 if (!isBeforeBaseTab)
7405                     baseIndex++;
7406
7407                 this.statuses.MoveTab(baseIndex, tab);
7408
7409                 this.ListTab.TabPages.Insert(baseIndex, tabPage);
7410             }
7411
7412             this.SaveConfigsTabs();
7413         }
7414
7415         private void MakeDirectMessageText()
7416         {
7417             var selectedPosts = this.CurrentTab.SelectedPosts;
7418             if (selectedPosts.Length > 1)
7419                 return;
7420
7421             var post = selectedPosts.Single();
7422             var text = $"D {post.ScreenName} {this.StatusText.Text}";
7423
7424             this.inReplyTo = null;
7425             this.StatusText.Text = text;
7426             this.StatusText.SelectionStart = text.Length;
7427             this.StatusText.Focus();
7428         }
7429
7430         private void MakeReplyText(bool atAll = false)
7431         {
7432             var selectedPosts = this.CurrentTab.SelectedPosts;
7433             if (selectedPosts.Any(x => x.IsDm))
7434             {
7435                 this.MakeDirectMessageText();
7436                 return;
7437             }
7438
7439             if (selectedPosts.Length == 1)
7440             {
7441                 var post = selectedPosts.Single();
7442                 var inReplyToStatusId = post.RetweetedId ?? post.StatusId;
7443                 var inReplyToScreenName = post.ScreenName;
7444                 this.inReplyTo = (inReplyToStatusId, inReplyToScreenName);
7445             }
7446             else
7447             {
7448                 this.inReplyTo = null;
7449             }
7450
7451             var selfScreenName = this.tw.Username;
7452             var targetScreenNames = new List<string>();
7453             foreach (var post in selectedPosts)
7454             {
7455                 if (post.ScreenName != selfScreenName)
7456                     targetScreenNames.Add(post.ScreenName);
7457
7458                 if (atAll)
7459                 {
7460                     foreach (var (_, screenName) in post.ReplyToList)
7461                     {
7462                         if (screenName != selfScreenName)
7463                             targetScreenNames.Add(screenName);
7464                     }
7465                 }
7466             }
7467
7468             if (this.inReplyTo != null)
7469             {
7470                 var (_, screenName) = this.inReplyTo.Value;
7471                 if (screenName == selfScreenName)
7472                     targetScreenNames.Insert(0, screenName);
7473             }
7474
7475             var text = this.StatusText.Text;
7476             foreach (var screenName in targetScreenNames.AsEnumerable().Reverse())
7477             {
7478                 var atText = $"@{screenName} ";
7479                 if (!text.Contains(atText))
7480                     text = atText + text;
7481             }
7482
7483             this.StatusText.Text = text;
7484             this.StatusText.SelectionStart = text.Length;
7485             this.StatusText.Focus();
7486         }
7487
7488         private void ListTab_MouseUp(object sender, MouseEventArgs e)
7489             => this.tabDrag = false;
7490
7491         private int iconCnt = 0;
7492         private int blinkCnt = 0;
7493         private bool blink = false;
7494
7495         private void RefreshTasktrayIcon()
7496         {
7497             void EnableTasktrayAnimation()
7498                 => this.TimerRefreshIcon.Enabled = true;
7499
7500             void DisableTasktrayAnimation()
7501                 => this.TimerRefreshIcon.Enabled = false;
7502
7503             var busyTasks = this.workerSemaphore.CurrentCount != MaxWorderThreads;
7504             if (busyTasks)
7505             {
7506                 this.iconCnt += 1;
7507                 if (this.iconCnt >= this.nIconRefresh.Length)
7508                     this.iconCnt = 0;
7509
7510                 this.NotifyIcon1.Icon = this.nIconRefresh[this.iconCnt];
7511                 this.myStatusError = false;
7512                 EnableTasktrayAnimation();
7513                 return;
7514             }
7515
7516             var replyIconType = SettingManager.Common.ReplyIconState;
7517             var reply = false;
7518             if (replyIconType != MyCommon.REPLY_ICONSTATE.None)
7519             {
7520                 var replyTab = this.statuses.GetTabByType<MentionsTabModel>();
7521                 if (replyTab != null && replyTab.UnreadCount > 0)
7522                     reply = true;
7523             }
7524
7525             if (replyIconType == MyCommon.REPLY_ICONSTATE.BlinkIcon && reply)
7526             {
7527                 this.blinkCnt += 1;
7528                 if (this.blinkCnt > 10)
7529                     this.blinkCnt = 0;
7530
7531                 if (this.blinkCnt == 0)
7532                     this.blink = !this.blink;
7533
7534                 this.NotifyIcon1.Icon = this.blink ? this.replyIconBlink : this.replyIcon;
7535                 EnableTasktrayAnimation();
7536                 return;
7537             }
7538
7539             DisableTasktrayAnimation();
7540
7541             this.iconCnt = 0;
7542             this.blinkCnt = 0;
7543             this.blink = false;
7544
7545             // 優先度:リプライ→エラー→オフライン→アイドル
7546             // エラーは更新アイコンでクリアされる
7547             if (replyIconType == MyCommon.REPLY_ICONSTATE.StaticIcon && reply)
7548                 this.NotifyIcon1.Icon = this.replyIcon;
7549             else if (this.myStatusError)
7550                 this.NotifyIcon1.Icon = this.nIconAtRed;
7551             else if (this.myStatusOnline)
7552                 this.NotifyIcon1.Icon = this.nIconAt;
7553             else
7554                 this.NotifyIcon1.Icon = this.nIconAtSmoke;
7555         }
7556
7557         private void TimerRefreshIcon_Tick(object sender, EventArgs e)
7558             => this.RefreshTasktrayIcon(); // 200ms
7559
7560         private void ContextMenuTabProperty_Opening(object sender, CancelEventArgs e)
7561         {
7562             // 右クリックの場合はタブ名が設定済。アプリケーションキーの場合は現在のタブを対象とする
7563             if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender != this.ContextMenuTabProperty)
7564                 this.rclickTabName = this.CurrentTabName;
7565
7566             if (this.statuses == null) return;
7567             if (this.statuses.Tabs == null) return;
7568
7569             if (!this.statuses.Tabs.TryGetValue(this.rclickTabName, out var tb))
7570                 return;
7571
7572             this.NotifyDispMenuItem.Checked = tb.Notify;
7573             this.NotifyTbMenuItem.Checked = tb.Notify;
7574
7575             this.soundfileListup = true;
7576             this.SoundFileComboBox.Items.Clear();
7577             this.SoundFileTbComboBox.Items.Clear();
7578             this.SoundFileComboBox.Items.Add("");
7579             this.SoundFileTbComboBox.Items.Add("");
7580             var oDir = new DirectoryInfo(Application.StartupPath + Path.DirectorySeparatorChar);
7581             if (Directory.Exists(Path.Combine(Application.StartupPath, "Sounds")))
7582             {
7583                 oDir = oDir.GetDirectories("Sounds")[0];
7584             }
7585             foreach (var oFile in oDir.GetFiles("*.wav"))
7586             {
7587                 this.SoundFileComboBox.Items.Add(oFile.Name);
7588                 this.SoundFileTbComboBox.Items.Add(oFile.Name);
7589             }
7590             var idx = this.SoundFileComboBox.Items.IndexOf(tb.SoundFile);
7591             if (idx == -1) idx = 0;
7592             this.SoundFileComboBox.SelectedIndex = idx;
7593             this.SoundFileTbComboBox.SelectedIndex = idx;
7594             this.soundfileListup = false;
7595             this.UreadManageMenuItem.Checked = tb.UnreadManage;
7596             this.UnreadMngTbMenuItem.Checked = tb.UnreadManage;
7597
7598             this.TabMenuControl(this.rclickTabName);
7599         }
7600
7601         private void TabMenuControl(string tabName)
7602         {
7603             var tabInfo = this.statuses.GetTabByName(tabName)!;
7604
7605             this.FilterEditMenuItem.Enabled = true;
7606             this.EditRuleTbMenuItem.Enabled = true;
7607
7608             if (tabInfo.IsDefaultTabType)
7609             {
7610                 this.ProtectTabMenuItem.Enabled = false;
7611                 this.ProtectTbMenuItem.Enabled = false;
7612             }
7613             else
7614             {
7615                 this.ProtectTabMenuItem.Enabled = true;
7616                 this.ProtectTbMenuItem.Enabled = true;
7617             }
7618
7619             if (tabInfo.IsDefaultTabType || tabInfo.Protected)
7620             {
7621                 this.ProtectTabMenuItem.Checked = true;
7622                 this.ProtectTbMenuItem.Checked = true;
7623                 this.DeleteTabMenuItem.Enabled = false;
7624                 this.DeleteTbMenuItem.Enabled = false;
7625             }
7626             else
7627             {
7628                 this.ProtectTabMenuItem.Checked = false;
7629                 this.ProtectTbMenuItem.Checked = false;
7630                 this.DeleteTabMenuItem.Enabled = true;
7631                 this.DeleteTbMenuItem.Enabled = true;
7632             }
7633         }
7634
7635         private void ProtectTabMenuItem_Click(object sender, EventArgs e)
7636         {
7637             var checkState = ((ToolStripMenuItem)sender).Checked;
7638
7639             // チェック状態を同期
7640             this.ProtectTbMenuItem.Checked = checkState;
7641             this.ProtectTabMenuItem.Checked = checkState;
7642
7643             // ロック中はタブの削除を無効化
7644             this.DeleteTabMenuItem.Enabled = !checkState;
7645             this.DeleteTbMenuItem.Enabled = !checkState;
7646
7647             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
7648             this.statuses.Tabs[this.rclickTabName].Protected = checkState;
7649
7650             this.SaveConfigsTabs();
7651         }
7652
7653         private void UreadManageMenuItem_Click(object sender, EventArgs e)
7654         {
7655             this.UreadManageMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
7656             this.UnreadMngTbMenuItem.Checked = this.UreadManageMenuItem.Checked;
7657
7658             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
7659             this.ChangeTabUnreadManage(this.rclickTabName, this.UreadManageMenuItem.Checked);
7660
7661             this.SaveConfigsTabs();
7662         }
7663
7664         public void ChangeTabUnreadManage(string tabName, bool isManage)
7665         {
7666             var idx = this.GetTabPageIndex(tabName);
7667             if (idx == -1)
7668                 return;
7669
7670             var tab = this.statuses.Tabs[tabName];
7671             tab.UnreadManage = isManage;
7672
7673             if (SettingManager.Common.TabIconDisp)
7674             {
7675                 var tabPage = this.ListTab.TabPages[idx];
7676                 if (tab.UnreadCount > 0)
7677                     tabPage.ImageIndex = 0;
7678                 else
7679                     tabPage.ImageIndex = -1;
7680             }
7681
7682             if (this.CurrentTabName == tabName)
7683             {
7684                 this.PurgeListViewItemCache();
7685                 this.CurrentListView.Refresh();
7686             }
7687
7688             this.SetMainWindowTitle();
7689             this.SetStatusLabelUrl();
7690             if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
7691         }
7692
7693         private void NotifyDispMenuItem_Click(object sender, EventArgs e)
7694         {
7695             this.NotifyDispMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
7696             this.NotifyTbMenuItem.Checked = this.NotifyDispMenuItem.Checked;
7697
7698             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
7699
7700             this.statuses.Tabs[this.rclickTabName].Notify = this.NotifyDispMenuItem.Checked;
7701
7702             this.SaveConfigsTabs();
7703         }
7704
7705         private void SoundFileComboBox_SelectedIndexChanged(object sender, EventArgs e)
7706         {
7707             if (this.soundfileListup || MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
7708
7709             this.statuses.Tabs[this.rclickTabName].SoundFile = (string)((ToolStripComboBox)sender).SelectedItem;
7710
7711             this.SaveConfigsTabs();
7712         }
7713
7714         private void DeleteTabMenuItem_Click(object sender, EventArgs e)
7715         {
7716             if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender == this.DeleteTbMenuItem)
7717                 this.rclickTabName = this.CurrentTabName;
7718
7719             this.RemoveSpecifiedTab(this.rclickTabName, true);
7720             this.SaveConfigsTabs();
7721         }
7722
7723         private void FilterEditMenuItem_Click(object sender, EventArgs e)
7724         {
7725             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) this.rclickTabName = this.statuses.HomeTab.TabName;
7726
7727             using (var fltDialog = new FilterDialog())
7728             {
7729                 fltDialog.Owner = this;
7730                 fltDialog.SetCurrent(this.rclickTabName);
7731                 fltDialog.ShowDialog(this);
7732             }
7733             this.TopMost = SettingManager.Common.AlwaysTop;
7734
7735             this.ApplyPostFilters();
7736             this.SaveConfigsTabs();
7737         }
7738
7739         private async void AddTabMenuItem_Click(object sender, EventArgs e)
7740         {
7741             string? tabName = null;
7742             MyCommon.TabUsageType tabUsage;
7743             using (var inputName = new InputTabName())
7744             {
7745                 inputName.TabName = this.statuses.MakeTabName("MyTab");
7746                 inputName.IsShowUsage = true;
7747                 inputName.ShowDialog();
7748                 if (inputName.DialogResult == DialogResult.Cancel) return;
7749                 tabName = inputName.TabName;
7750                 tabUsage = inputName.Usage;
7751             }
7752             this.TopMost = SettingManager.Common.AlwaysTop;
7753             if (!MyCommon.IsNullOrEmpty(tabName))
7754             {
7755                 // List対応
7756                 ListElement? list = null;
7757                 if (tabUsage == MyCommon.TabUsageType.Lists)
7758                 {
7759                     using var listAvail = new ListAvailable();
7760                     if (listAvail.ShowDialog(this) == DialogResult.Cancel)
7761                         return;
7762                     if (listAvail.SelectedList == null)
7763                         return;
7764                     list = listAvail.SelectedList;
7765                 }
7766
7767                 TabModel tab;
7768                 switch (tabUsage)
7769                 {
7770                     case MyCommon.TabUsageType.UserDefined:
7771                         tab = new FilterTabModel(tabName);
7772                         break;
7773                     case MyCommon.TabUsageType.PublicSearch:
7774                         tab = new PublicSearchTabModel(tabName);
7775                         break;
7776                     case MyCommon.TabUsageType.Lists:
7777                         tab = new ListTimelineTabModel(tabName, list!);
7778                         break;
7779                     default:
7780                         return;
7781                 }
7782
7783                 if (!this.statuses.AddTab(tab) || !this.AddNewTab(tab, startup: false))
7784                 {
7785                     var tmp = string.Format(Properties.Resources.AddTabMenuItem_ClickText1, tabName);
7786                     MessageBox.Show(tmp, Properties.Resources.AddTabMenuItem_ClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
7787                 }
7788                 else
7789                 {
7790                     // 成功
7791                     this.SaveConfigsTabs();
7792
7793                     var tabIndex = this.statuses.Tabs.Count - 1;
7794
7795                     if (tabUsage == MyCommon.TabUsageType.PublicSearch)
7796                     {
7797                         this.ListTab.SelectedIndex = tabIndex;
7798                         this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus();
7799                     }
7800                     if (tabUsage == MyCommon.TabUsageType.Lists)
7801                     {
7802                         this.ListTab.SelectedIndex = tabIndex;
7803                         await this.RefreshTabAsync(this.CurrentTab);
7804                     }
7805                 }
7806             }
7807         }
7808
7809         private void TabMenuItem_Click(object sender, EventArgs e)
7810         {
7811             // 選択発言を元にフィルタ追加
7812             foreach (var post in this.CurrentTab.SelectedPosts)
7813             {
7814                 // タブ選択(or追加)
7815                 if (!this.SelectTab(out var tab))
7816                     return;
7817
7818                 using (var fltDialog = new FilterDialog())
7819                 {
7820                     fltDialog.Owner = this;
7821                     fltDialog.SetCurrent(tab.TabName);
7822
7823                     if (post.RetweetedBy == null)
7824                     {
7825                         fltDialog.AddNewFilter(post.ScreenName, post.TextFromApi);
7826                     }
7827                     else
7828                     {
7829                         fltDialog.AddNewFilter(post.RetweetedBy, post.TextFromApi);
7830                     }
7831                     fltDialog.ShowDialog(this);
7832                 }
7833
7834                 this.TopMost = SettingManager.Common.AlwaysTop;
7835             }
7836
7837             this.ApplyPostFilters();
7838             this.SaveConfigsTabs();
7839         }
7840
7841         protected override bool ProcessDialogKey(Keys keyData)
7842         {
7843             // TextBox1でEnterを押してもビープ音が鳴らないようにする
7844             if ((keyData & Keys.KeyCode) == Keys.Enter)
7845             {
7846                 if (this.StatusText.Focused)
7847                 {
7848                     var newLine = false;
7849                     var post = false;
7850
7851                     if (SettingManager.Common.PostCtrlEnter) // Ctrl+Enter投稿時
7852                     {
7853                         if (this.StatusText.Multiline)
7854                         {
7855                             if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true;
7856
7857                             if ((keyData & Keys.Control) == Keys.Control) post = true;
7858                         }
7859                         else
7860                         {
7861                             if ((keyData & Keys.Control) == Keys.Control) post = true;
7862                         }
7863                     }
7864                     else if (SettingManager.Common.PostShiftEnter) // SHift+Enter投稿時
7865                     {
7866                         if (this.StatusText.Multiline)
7867                         {
7868                             if ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) != Keys.Shift) newLine = true;
7869
7870                             if ((keyData & Keys.Shift) == Keys.Shift) post = true;
7871                         }
7872                         else
7873                         {
7874                             if ((keyData & Keys.Shift) == Keys.Shift) post = true;
7875                         }
7876                     }
7877                     else // Enter投稿時
7878                     {
7879                         if (this.StatusText.Multiline)
7880                         {
7881                             if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true;
7882
7883                             if (((keyData & Keys.Control) != Keys.Control && (keyData & Keys.Shift) != Keys.Shift) ||
7884                                 ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) == Keys.Shift)) post = true;
7885                         }
7886                         else
7887                         {
7888                             if (((keyData & Keys.Shift) == Keys.Shift) ||
7889                                 (((keyData & Keys.Control) != Keys.Control) &&
7890                                 ((keyData & Keys.Shift) != Keys.Shift))) post = true;
7891                         }
7892                     }
7893
7894                     if (newLine)
7895                     {
7896                         var pos1 = this.StatusText.SelectionStart;
7897                         if (this.StatusText.SelectionLength > 0)
7898                         {
7899                             this.StatusText.Text = this.StatusText.Text.Remove(pos1, this.StatusText.SelectionLength);  // 選択状態文字列削除
7900                         }
7901                         this.StatusText.Text = this.StatusText.Text.Insert(pos1, Environment.NewLine);  // 改行挿入
7902                         this.StatusText.SelectionStart = pos1 + Environment.NewLine.Length;    // カーソルを改行の次の文字へ移動
7903                         return true;
7904                     }
7905                     else if (post)
7906                     {
7907                         this.PostButton_Click(this.PostButton, EventArgs.Empty);
7908                         return true;
7909                     }
7910                 }
7911                 else
7912                 {
7913                     var tab = this.CurrentTab;
7914                     if (tab.TabType == MyCommon.TabUsageType.PublicSearch)
7915                     {
7916                         var tabPage = this.CurrentTabPage;
7917                         if (tabPage.Controls["panelSearch"].Controls["comboSearch"].Focused ||
7918                             tabPage.Controls["panelSearch"].Controls["comboLang"].Focused)
7919                         {
7920                             this.SearchButton_Click(tabPage.Controls["panelSearch"].Controls["comboSearch"], EventArgs.Empty);
7921                             return true;
7922                         }
7923                     }
7924                 }
7925             }
7926
7927             return base.ProcessDialogKey(keyData);
7928         }
7929
7930         private void ReplyAllStripMenuItem_Click(object sender, EventArgs e)
7931             => this.MakeReplyText(atAll: true);
7932
7933         private void IDRuleMenuItem_Click(object sender, EventArgs e)
7934         {
7935             var tab = this.CurrentTab;
7936             var selectedPosts = tab.SelectedPosts;
7937
7938             // 未選択なら処理終了
7939             if (selectedPosts.Length == 0)
7940                 return;
7941
7942             var screenNameArray = selectedPosts
7943                 .Select(x => x.RetweetedBy ?? x.ScreenName)
7944                 .ToArray();
7945
7946             this.AddFilterRuleByScreenName(screenNameArray);
7947
7948             if (screenNameArray.Length != 0)
7949             {
7950                 var atids = new List<string>();
7951                 foreach (var screenName in screenNameArray)
7952                 {
7953                     atids.Add("@" + screenName);
7954                 }
7955                 var cnt = this.AtIdSupl.ItemCount;
7956                 this.AtIdSupl.AddRangeItem(atids.ToArray());
7957                 if (this.AtIdSupl.ItemCount != cnt)
7958                     this.MarkSettingAtIdModified();
7959             }
7960         }
7961
7962         private void SourceRuleMenuItem_Click(object sender, EventArgs e)
7963         {
7964             var tab = this.CurrentTab;
7965             var selectedPosts = tab.SelectedPosts;
7966
7967             if (selectedPosts.Length == 0)
7968                 return;
7969
7970             var sourceArray = selectedPosts.Select(x => x.Source).ToArray();
7971
7972             this.AddFilterRuleBySource(sourceArray);
7973         }
7974
7975         public void AddFilterRuleByScreenName(params string[] screenNameArray)
7976         {
7977             // タブ選択(or追加)
7978             if (!this.SelectTab(out var tab)) return;
7979
7980             bool mv;
7981             bool mk;
7982             if (tab.TabType != MyCommon.TabUsageType.Mute)
7983             {
7984                 this.MoveOrCopy(out mv, out mk);
7985             }
7986             else
7987             {
7988                 // ミュートタブでは常に MoveMatches を true にする
7989                 mv = true;
7990                 mk = false;
7991             }
7992
7993             foreach (var screenName in screenNameArray)
7994             {
7995                 tab.AddFilter(new PostFilterRule
7996                 {
7997                     FilterName = screenName,
7998                     UseNameField = true,
7999                     MoveMatches = mv,
8000                     MarkMatches = mk,
8001                     UseRegex = false,
8002                     FilterByUrl = false,
8003                 });
8004             }
8005
8006             this.ApplyPostFilters();
8007             this.SaveConfigsTabs();
8008         }
8009
8010         public void AddFilterRuleBySource(params string[] sourceArray)
8011         {
8012             // タブ選択ダイアログを表示(or追加)
8013             if (!this.SelectTab(out var filterTab))
8014                 return;
8015
8016             bool mv;
8017             bool mk;
8018             if (filterTab.TabType != MyCommon.TabUsageType.Mute)
8019             {
8020                 // フィルタ動作選択ダイアログを表示(移動/コピー, マーク有無)
8021                 this.MoveOrCopy(out mv, out mk);
8022             }
8023             else
8024             {
8025                 // ミュートタブでは常に MoveMatches を true にする
8026                 mv = true;
8027                 mk = false;
8028             }
8029
8030             // 振り分けルールに追加するSource
8031             foreach (var source in sourceArray)
8032             {
8033                 filterTab.AddFilter(new PostFilterRule
8034                 {
8035                     FilterSource = source,
8036                     MoveMatches = mv,
8037                     MarkMatches = mk,
8038                     UseRegex = false,
8039                     FilterByUrl = false,
8040                 });
8041             }
8042
8043             this.ApplyPostFilters();
8044             this.SaveConfigsTabs();
8045         }
8046
8047         private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab)
8048         {
8049             do
8050             {
8051                 tab = null;
8052
8053                 // 振り分け先タブ選択
8054                 using (var dialog = new TabsDialog(this.statuses))
8055                 {
8056                     if (dialog.ShowDialog(this) == DialogResult.Cancel) return false;
8057
8058                     tab = dialog.SelectedTab;
8059                 }
8060
8061                 this.CurrentTabPage.Focus();
8062                 // 新規タブを選択→タブ作成
8063                 if (tab == null)
8064                 {
8065                     string tabName;
8066                     using (var inputName = new InputTabName())
8067                     {
8068                         inputName.TabName = this.statuses.MakeTabName("MyTab");
8069                         inputName.ShowDialog();
8070                         if (inputName.DialogResult == DialogResult.Cancel) return false;
8071                         tabName = inputName.TabName;
8072                     }
8073                     this.TopMost = SettingManager.Common.AlwaysTop;
8074                     if (!MyCommon.IsNullOrEmpty(tabName))
8075                     {
8076                         var newTab = new FilterTabModel(tabName);
8077                         if (!this.statuses.AddTab(newTab) || !this.AddNewTab(newTab, startup: false))
8078                         {
8079                             var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText2, tabName);
8080                             MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText3, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
8081                             // もう一度タブ名入力
8082                         }
8083                         else
8084                         {
8085                             tab = newTab;
8086                             return true;
8087                         }
8088                     }
8089                 }
8090                 else
8091                 {
8092                     // 既存タブを選択
8093                     return true;
8094                 }
8095             }
8096             while (true);
8097         }
8098
8099         private void MoveOrCopy(out bool move, out bool mark)
8100         {
8101             {
8102                 // 移動するか?
8103                 var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText4, Environment.NewLine);
8104                 if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText5, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
8105                     move = false;
8106                 else
8107                     move = true;
8108             }
8109             if (!move)
8110             {
8111                 // マークするか?
8112                 var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText6, Environment.NewLine);
8113                 if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText7, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
8114                     mark = true;
8115                 else
8116                     mark = false;
8117             }
8118             else
8119             {
8120                 mark = false;
8121             }
8122         }
8123
8124         private void CopySTOTMenuItem_Click(object sender, EventArgs e)
8125             => this.CopyStot();
8126
8127         private void CopyURLMenuItem_Click(object sender, EventArgs e)
8128             => this.CopyIdUri();
8129
8130         private void SelectAllMenuItem_Click(object sender, EventArgs e)
8131         {
8132             if (this.StatusText.Focused)
8133             {
8134                 // 発言欄でのCtrl+A
8135                 this.StatusText.SelectAll();
8136             }
8137             else
8138             {
8139                 // ListView上でのCtrl+A
8140                 NativeMethods.SelectAllItems(this.CurrentListView);
8141             }
8142         }
8143
8144         private void MoveMiddle()
8145         {
8146             ListViewItem item;
8147             int idx1;
8148             int idx2;
8149
8150             var listView = this.CurrentListView;
8151             if (listView.SelectedIndices.Count == 0) return;
8152
8153             var idx = listView.SelectedIndices[0];
8154
8155             item = listView.GetItemAt(0, 25);
8156             if (item == null)
8157                 idx1 = 0;
8158             else
8159                 idx1 = item.Index;
8160
8161             item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
8162             if (item == null)
8163                 idx2 = listView.VirtualListSize - 1;
8164             else
8165                 idx2 = item.Index;
8166
8167             idx -= Math.Abs(idx1 - idx2) / 2;
8168             if (idx < 0) idx = 0;
8169
8170             listView.EnsureVisible(listView.VirtualListSize - 1);
8171             listView.EnsureVisible(idx);
8172         }
8173
8174         private async void OpenURLMenuItem_Click(object sender, EventArgs e)
8175         {
8176             var linkElements = this.tweetDetailsView.GetLinkElements();
8177
8178             if (linkElements.Length == 0)
8179                 return;
8180
8181             var links = new List<OpenUrlItem>(linkElements.Length);
8182
8183             foreach (var linkElm in linkElements)
8184             {
8185                 var displayUrl = linkElm.GetAttribute("title");
8186                 var href = linkElm.GetAttribute("href");
8187                 var linkedText = linkElm.InnerText;
8188
8189                 if (MyCommon.IsNullOrEmpty(displayUrl))
8190                     displayUrl = href;
8191
8192                 links.Add(new OpenUrlItem(linkedText, displayUrl, href));
8193             }
8194
8195             string selectedUrl;
8196             bool isReverseSettings;
8197
8198             if (links.Count == 1)
8199             {
8200                 // ツイートに含まれる URL が 1 つのみの場合
8201                 //   => OpenURL ダイアログを表示せずにリンクを開く
8202                 selectedUrl = links[0].Href;
8203
8204                 // Ctrl+E で呼ばれた場合を考慮し isReverseSettings の判定を行わない
8205                 isReverseSettings = false;
8206             }
8207             else
8208             {
8209                 // ツイートに含まれる URL が複数ある場合
8210                 //   => OpenURL を表示しユーザーが選択したリンクを開く
8211                 this.urlDialog.ClearUrl();
8212
8213                 foreach (var link in links)
8214                     this.urlDialog.AddUrl(link);
8215
8216                 if (this.urlDialog.ShowDialog(this) != DialogResult.OK)
8217                     return;
8218
8219                 this.TopMost = SettingManager.Common.AlwaysTop;
8220
8221                 selectedUrl = this.urlDialog.SelectedUrl;
8222
8223                 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
8224                 isReverseSettings = MyCommon.IsKeyDown(Keys.Control);
8225             }
8226
8227             await this.OpenUriAsync(new Uri(selectedUrl), isReverseSettings);
8228         }
8229
8230         private void ClearTabMenuItem_Click(object sender, EventArgs e)
8231         {
8232             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
8233             this.ClearTab(this.rclickTabName, true);
8234         }
8235
8236         private void ClearTab(string tabName, bool showWarning)
8237         {
8238             if (showWarning)
8239             {
8240                 var tmp = string.Format(Properties.Resources.ClearTabMenuItem_ClickText1, Environment.NewLine);
8241                 if (MessageBox.Show(tmp, tabName + " " + Properties.Resources.ClearTabMenuItem_ClickText2, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
8242                 {
8243                     return;
8244                 }
8245             }
8246
8247             this.statuses.ClearTabIds(tabName);
8248             if (this.CurrentTabName == tabName)
8249             {
8250                 this.anchorPost = null;
8251                 this.anchorFlag = false;
8252                 this.PurgeListViewItemCache();
8253             }
8254
8255             var tabIndex = this.statuses.Tabs.IndexOf(tabName);
8256             var tabPage = this.ListTab.TabPages[tabIndex];
8257             tabPage.ImageIndex = -1;
8258
8259             var listView = (DetailsListView)tabPage.Tag;
8260             listView.VirtualListSize = 0;
8261
8262             if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
8263
8264             this.SetMainWindowTitle();
8265             this.SetStatusLabelUrl();
8266         }
8267
8268         private static long followers = 0;
8269
8270         private void SetMainWindowTitle()
8271         {
8272             // メインウインドウタイトルの書き換え
8273             var ttl = new StringBuilder(256);
8274             var ur = 0;
8275             var al = 0;
8276             if (SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.None &&
8277                 SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Post &&
8278                 SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver &&
8279                 SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus)
8280             {
8281                 foreach (var tab in this.statuses.Tabs)
8282                 {
8283                     ur += tab.UnreadCount;
8284                     al += tab.AllCount;
8285                 }
8286             }
8287
8288             if (SettingManager.Common.DispUsername) ttl.Append(this.tw.Username).Append(" - ");
8289             ttl.Append(ApplicationSettings.ApplicationName);
8290             ttl.Append("  ");
8291             switch (SettingManager.Common.DispLatestPost)
8292             {
8293                 case MyCommon.DispTitleEnum.Ver:
8294                     ttl.Append("Ver:").Append(MyCommon.GetReadableVersion());
8295                     break;
8296                 case MyCommon.DispTitleEnum.Post:
8297                     if (this.history != null && this.history.Count > 1)
8298                         ttl.Append(this.history[this.history.Count - 2].Status.Replace("\r\n", " "));
8299                     break;
8300                 case MyCommon.DispTitleEnum.UnreadRepCount:
8301                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText1, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount);
8302                     break;
8303                 case MyCommon.DispTitleEnum.UnreadAllCount:
8304                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText2, ur);
8305                     break;
8306                 case MyCommon.DispTitleEnum.UnreadAllRepCount:
8307                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText3, ur, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount);
8308                     break;
8309                 case MyCommon.DispTitleEnum.UnreadCountAllCount:
8310                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText4, ur, al);
8311                     break;
8312                 case MyCommon.DispTitleEnum.OwnStatus:
8313                     if (followers == 0 && this.tw.FollowersCount > 0) followers = this.tw.FollowersCount;
8314                     ttl.AppendFormat(Properties.Resources.OwnStatusTitle, this.tw.StatusesCount, this.tw.FriendsCount, this.tw.FollowersCount, this.tw.FollowersCount - followers);
8315                     break;
8316             }
8317
8318             try
8319             {
8320                 this.Text = ttl.ToString();
8321             }
8322             catch (AccessViolationException)
8323             {
8324                 // 原因不明。ポスト内容に依存か?たまーに発生するが再現せず。
8325             }
8326         }
8327
8328         private string GetStatusLabelText()
8329         {
8330             // ステータス欄にカウント表示
8331             // タブ未読数/タブ発言数 全未読数/総発言数 (未読@+未読DM数)
8332             if (this.statuses == null) return "";
8333             var tbRep = this.statuses.MentionTab;
8334             var tbDm = this.statuses.DirectMessageTab;
8335             if (tbRep == null || tbDm == null) return "";
8336             var urat = tbRep.UnreadCount + tbDm.UnreadCount;
8337             var ur = 0;
8338             var al = 0;
8339             var tur = 0;
8340             var tal = 0;
8341             var slbl = new StringBuilder(256);
8342             try
8343             {
8344                 foreach (var tab in this.statuses.Tabs)
8345                 {
8346                     ur += tab.UnreadCount;
8347                     al += tab.AllCount;
8348                     if (tab.TabName == this.CurrentTabName)
8349                     {
8350                         tur = tab.UnreadCount;
8351                         tal = tab.AllCount;
8352                     }
8353                 }
8354             }
8355             catch (Exception)
8356             {
8357                 return "";
8358             }
8359
8360             this.unreadCounter = ur;
8361             this.unreadAtCounter = urat;
8362
8363             var homeTab = this.statuses.HomeTab;
8364
8365             slbl.AppendFormat(Properties.Resources.SetStatusLabelText1, tur, tal, ur, al, urat, this.postTimestamps.Count, this.favTimestamps.Count, homeTab.TweetsPerHour);
8366             if (SettingManager.Common.TimelinePeriod == 0)
8367             {
8368                 slbl.Append(Properties.Resources.SetStatusLabelText2);
8369             }
8370             else
8371             {
8372                 slbl.Append(SettingManager.Common.TimelinePeriod + Properties.Resources.SetStatusLabelText3);
8373             }
8374             return slbl.ToString();
8375         }
8376
8377         private async void TwitterApiStatus_AccessLimitUpdated(object sender, EventArgs e)
8378         {
8379             try
8380             {
8381                 if (this.InvokeRequired && !this.IsDisposed)
8382                 {
8383                     await this.InvokeAsync(() => this.TwitterApiStatus_AccessLimitUpdated(sender, e));
8384                 }
8385                 else
8386                 {
8387                     var endpointName = ((TwitterApiStatus.AccessLimitUpdatedEventArgs)e).EndpointName;
8388                     this.SetApiStatusLabel(endpointName);
8389                 }
8390             }
8391             catch (ObjectDisposedException)
8392             {
8393                 return;
8394             }
8395             catch (InvalidOperationException)
8396             {
8397                 return;
8398             }
8399         }
8400
8401         private void SetApiStatusLabel(string? endpointName = null)
8402         {
8403             var tabType = this.CurrentTab.TabType;
8404
8405             if (endpointName == null)
8406             {
8407                 // 表示中のタブに応じて更新
8408                 endpointName = tabType switch
8409                 {
8410                     MyCommon.TabUsageType.Home => "/statuses/home_timeline",
8411                     MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
8412                     MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
8413                     MyCommon.TabUsageType.Favorites => "/favorites/list",
8414                     MyCommon.TabUsageType.DirectMessage => "/direct_messages/events/list",
8415                     MyCommon.TabUsageType.UserTimeline => "/statuses/user_timeline",
8416                     MyCommon.TabUsageType.Lists => "/lists/statuses",
8417                     MyCommon.TabUsageType.PublicSearch => "/search/tweets",
8418                     MyCommon.TabUsageType.Related => "/statuses/show/:id",
8419                     _ => null,
8420                 };
8421                 this.toolStripApiGauge.ApiEndpoint = endpointName;
8422             }
8423             else
8424             {
8425                 // 表示中のタブに関連する endpoint であれば更新
8426                 var update = endpointName switch
8427                 {
8428                     "/statuses/home_timeline" => tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined,
8429                     "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
8430                     "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
8431                     "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
8432                     "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
8433                     "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
8434                     "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
8435                     "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
8436                     _ => false,
8437                 };
8438                 if (update)
8439                 {
8440                     this.toolStripApiGauge.ApiEndpoint = endpointName;
8441                 }
8442             }
8443         }
8444
8445         private void SetStatusLabelUrl()
8446             => this.StatusLabelUrl.Text = this.GetStatusLabelText();
8447
8448         public void SetStatusLabel(string text)
8449             => this.StatusLabel.Text = text;
8450
8451         private void SetNotifyIconText()
8452         {
8453             var ur = new StringBuilder(64);
8454
8455             // タスクトレイアイコンのツールチップテキスト書き換え
8456             // Tween [未読/@]
8457             ur.Remove(0, ur.Length);
8458             if (SettingManager.Common.DispUsername)
8459             {
8460                 ur.Append(this.tw.Username);
8461                 ur.Append(" - ");
8462             }
8463             ur.Append(ApplicationSettings.ApplicationName);
8464 #if DEBUG
8465             ur.Append("(Debug Build)");
8466 #endif
8467             if (this.unreadCounter != -1 && this.unreadAtCounter != -1)
8468             {
8469                 ur.Append(" [");
8470                 ur.Append(this.unreadCounter);
8471                 ur.Append("/@");
8472                 ur.Append(this.unreadAtCounter);
8473                 ur.Append("]");
8474             }
8475             this.NotifyIcon1.Text = ur.ToString();
8476         }
8477
8478         internal void CheckReplyTo(string statusText)
8479         {
8480             MatchCollection m;
8481             // ハッシュタグの保存
8482             m = Regex.Matches(statusText, Twitter.Hashtag, RegexOptions.IgnoreCase);
8483             var hstr = "";
8484             foreach (Match hm in m)
8485             {
8486                 if (!hstr.Contains("#" + hm.Result("$3") + " "))
8487                 {
8488                     hstr += "#" + hm.Result("$3") + " ";
8489                     this.HashSupl.AddItem("#" + hm.Result("$3"));
8490                 }
8491             }
8492             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && !hstr.Contains(this.HashMgr.UseHash + " "))
8493             {
8494                 hstr += this.HashMgr.UseHash;
8495             }
8496             if (!MyCommon.IsNullOrEmpty(hstr)) this.HashMgr.AddHashToHistory(hstr.Trim(), false);
8497
8498             // 本当にリプライ先指定すべきかどうかの判定
8499             m = Regex.Matches(statusText, "(^|[ -/:-@[-^`{-~])(?<id>@[a-zA-Z0-9_]+)");
8500
8501             if (SettingManager.Common.UseAtIdSupplement)
8502             {
8503                 var bCnt = this.AtIdSupl.ItemCount;
8504                 foreach (Match mid in m)
8505                 {
8506                     this.AtIdSupl.AddItem(mid.Result("${id}"));
8507                 }
8508                 if (bCnt != this.AtIdSupl.ItemCount)
8509                     this.MarkSettingAtIdModified();
8510             }
8511
8512             // リプライ先ステータスIDの指定がない場合は指定しない
8513             if (this.inReplyTo == null)
8514                 return;
8515
8516             // 通常Reply
8517             // 次の条件を満たす場合に in_reply_to_status_id 指定
8518             // 1. Twitterによりリンクと判定される @idが文中に1つ含まれる (2009/5/28 リンク化される@IDのみカウントするように修正)
8519             // 2. リプライ先ステータスIDが設定されている(リストをダブルクリックで返信している)
8520             // 3. 文中に含まれた@idがリプライ先のポスト者のIDと一致する
8521
8522             if (m != null)
8523             {
8524                 var inReplyToScreenName = this.inReplyTo.Value.ScreenName;
8525                 if (statusText.StartsWith("@", StringComparison.Ordinal))
8526                 {
8527                     if (statusText.StartsWith("@" + inReplyToScreenName, StringComparison.Ordinal)) return;
8528                 }
8529                 else
8530                 {
8531                     foreach (Match mid in m)
8532                     {
8533                         if (statusText.Contains("RT " + mid.Result("${id}") + ":") && mid.Result("${id}") == "@" + inReplyToScreenName) return;
8534                     }
8535                 }
8536             }
8537
8538             this.inReplyTo = null;
8539         }
8540
8541         private void TweenMain_Resize(object sender, EventArgs e)
8542         {
8543             if (!this.initialLayout && SettingManager.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
8544             {
8545                 this.Visible = false;
8546             }
8547             if (this.initialLayout && SettingManager.Local != null && this.WindowState == FormWindowState.Normal && this.Visible)
8548             {
8549                 // 現在の DPI と設定保存時の DPI との比を取得する
8550                 var configScaleFactor = SettingManager.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
8551
8552                 this.ClientSize = ScaleBy(configScaleFactor, SettingManager.Local.FormSize);
8553
8554                 // Splitterの位置設定
8555                 var splitterDistance = ScaleBy(configScaleFactor.Height, SettingManager.Local.SplitterDistance);
8556                 if (splitterDistance > this.SplitContainer1.Panel1MinSize &&
8557                     splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth)
8558                 {
8559                     this.SplitContainer1.SplitterDistance = splitterDistance;
8560                 }
8561
8562                 // 発言欄複数行
8563                 this.StatusText.Multiline = SettingManager.Local.StatusMultiline;
8564                 if (this.StatusText.Multiline)
8565                 {
8566                     var statusTextHeight = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight);
8567                     var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
8568                     if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth)
8569                     {
8570                         this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
8571                     }
8572                     this.StatusText.Height = statusTextHeight;
8573                 }
8574                 else
8575                 {
8576                     if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0)
8577                     {
8578                         this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
8579                     }
8580                 }
8581
8582                 var previewDistance = ScaleBy(configScaleFactor.Width, SettingManager.Local.PreviewDistance);
8583                 if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth)
8584                 {
8585                     this.SplitContainer3.SplitterDistance = previewDistance;
8586                 }
8587
8588                 // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない
8589                 this.SplitContainer3.Panel2Collapsed = true;
8590
8591                 this.initialLayout = false;
8592             }
8593             if (this.WindowState != FormWindowState.Minimized)
8594             {
8595                 this.formWindowState = this.WindowState;
8596             }
8597         }
8598
8599         private void PlaySoundMenuItem_CheckedChanged(object sender, EventArgs e)
8600         {
8601             this.PlaySoundMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
8602             this.PlaySoundFileMenuItem.Checked = this.PlaySoundMenuItem.Checked;
8603             if (this.PlaySoundMenuItem.Checked)
8604             {
8605                 SettingManager.Common.PlaySound = true;
8606             }
8607             else
8608             {
8609                 SettingManager.Common.PlaySound = false;
8610             }
8611             this.MarkSettingCommonModified();
8612         }
8613
8614         private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e)
8615         {
8616             if (this.initialLayout)
8617                 return;
8618
8619             int splitterDistance;
8620             switch (this.WindowState)
8621             {
8622                 case FormWindowState.Normal:
8623                     splitterDistance = this.SplitContainer1.SplitterDistance;
8624                     break;
8625                 case FormWindowState.Maximized:
8626                     // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する
8627                     var normalContainerHeight = this.mySize.Height - this.ToolStripContainer1.TopToolStripPanel.Height - this.ToolStripContainer1.BottomToolStripPanel.Height;
8628                     splitterDistance = this.SplitContainer1.SplitterDistance - (this.SplitContainer1.Height - normalContainerHeight);
8629                     splitterDistance = Math.Min(splitterDistance, normalContainerHeight - this.SplitContainer1.SplitterWidth - this.SplitContainer1.Panel2MinSize);
8630                     break;
8631                 default:
8632                     return;
8633             }
8634
8635             this.mySpDis = splitterDistance;
8636             this.MarkSettingLocalModified();
8637         }
8638
8639         private async Task DoRepliedStatusOpen()
8640         {
8641             var currentPost = this.CurrentPost;
8642             if (this.ExistCurrentPost && currentPost != null && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)
8643             {
8644                 if (MyCommon.IsKeyDown(Keys.Shift))
8645                 {
8646                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
8647                     return;
8648                 }
8649                 if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost))
8650                 {
8651                     MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
8652                 }
8653                 else
8654                 {
8655                     foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch))
8656                     {
8657                         if (tb == null || !tb.Contains(currentPost.InReplyToStatusId.Value)) break;
8658                         repPost = tb.Posts[currentPost.InReplyToStatusId.Value];
8659                         MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
8660                         return;
8661                     }
8662                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
8663                 }
8664             }
8665         }
8666
8667         private async void RepliedStatusOpenMenuItem_Click(object sender, EventArgs e)
8668             => await this.DoRepliedStatusOpen();
8669
8670         private void SplitContainer2_Panel2_Resize(object sender, EventArgs e)
8671         {
8672             if (this.initialLayout)
8673                 return; // SettingLocal の反映が完了するまで multiline の判定を行わない
8674
8675             var multiline = this.SplitContainer2.Panel2.Height > this.SplitContainer2.Panel2MinSize + 2;
8676             if (multiline != this.StatusText.Multiline)
8677             {
8678                 this.StatusText.Multiline = multiline;
8679                 SettingManager.Local.StatusMultiline = multiline;
8680                 this.MarkSettingLocalModified();
8681             }
8682         }
8683
8684         private void StatusText_MultilineChanged(object sender, EventArgs e)
8685         {
8686             if (this.StatusText.Multiline)
8687                 this.StatusText.ScrollBars = ScrollBars.Vertical;
8688             else
8689                 this.StatusText.ScrollBars = ScrollBars.None;
8690
8691             if (!this.initialLayout)
8692                 this.MarkSettingLocalModified();
8693         }
8694
8695         private void MultiLineMenuItem_Click(object sender, EventArgs e)
8696         {
8697             // 発言欄複数行
8698             var menuItemChecked = ((ToolStripMenuItem)sender).Checked;
8699             this.StatusText.Multiline = menuItemChecked;
8700             SettingManager.Local.StatusMultiline = menuItemChecked;
8701             if (menuItemChecked)
8702             {
8703                 if (this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth < 0)
8704                     this.SplitContainer2.SplitterDistance = 0;
8705                 else
8706                     this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth;
8707             }
8708             else
8709             {
8710                 this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
8711             }
8712             this.MarkSettingLocalModified();
8713         }
8714
8715         private async Task<bool> UrlConvertAsync(MyCommon.UrlConverter converterType)
8716         {
8717             if (converterType == MyCommon.UrlConverter.Bitly || converterType == MyCommon.UrlConverter.Jmp)
8718             {
8719                 // OAuth2 アクセストークンまたは API キー (旧方式) のいずれも設定されていなければ短縮しない
8720                 if (MyCommon.IsNullOrEmpty(SettingManager.Common.BitlyAccessToken) &&
8721                     (MyCommon.IsNullOrEmpty(SettingManager.Common.BilyUser) || MyCommon.IsNullOrEmpty(SettingManager.Common.BitlyPwd)))
8722                 {
8723                     MessageBox.Show(this, Properties.Resources.UrlConvert_BitlyAuthRequired, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
8724                     return false;
8725                 }
8726             }
8727
8728             // Converter_Type=Nicomsの場合は、nicovideoのみ短縮する
8729             // 参考資料 RFC3986 Uniform Resource Identifier (URI): Generic Syntax
8730             // Appendix A.  Collected ABNF for URI
8731             // http://www.ietf.org/rfc/rfc3986.txt
8732
8733             const string nico = @"^https?://[a-z]+\.(nicovideo|niconicommons|nicolive)\.jp/[a-z]+/[a-z0-9]+$";
8734
8735             string result;
8736             if (this.StatusText.SelectionLength > 0)
8737             {
8738                 var tmp = this.StatusText.SelectedText;
8739                 // httpから始まらない場合、ExcludeStringで指定された文字列で始まる場合は対象としない
8740                 if (tmp.StartsWith("http", StringComparison.OrdinalIgnoreCase))
8741                 {
8742                     // 文字列が選択されている場合はその文字列について処理
8743
8744                     // nico.ms使用、nicovideoにマッチしたら変換
8745                     if (SettingManager.Common.Nicoms && Regex.IsMatch(tmp, nico))
8746                     {
8747                         result = Nicoms.Shorten(tmp);
8748                     }
8749                     else if (converterType != MyCommon.UrlConverter.Nicoms)
8750                     {
8751                         // 短縮URL変換
8752                         try
8753                         {
8754                             var srcUri = new Uri(tmp);
8755                             var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri);
8756                             result = resultUri.AbsoluteUri;
8757                         }
8758                         catch (WebApiException e)
8759                         {
8760                             this.StatusLabel.Text = converterType + ":" + e.Message;
8761                             return false;
8762                         }
8763                         catch (UriFormatException e)
8764                         {
8765                             this.StatusLabel.Text = converterType + ":" + e.Message;
8766                             return false;
8767                         }
8768                     }
8769                     else
8770                     {
8771                         return true;
8772                     }
8773
8774                     if (!MyCommon.IsNullOrEmpty(result))
8775                     {
8776                         var undotmp = new UrlUndo();
8777
8778                         // 短縮 URL が生成されるまでの間に投稿欄から元の URL が削除されていたら中断する
8779                         var origUrlIndex = this.StatusText.Text.IndexOf(tmp, StringComparison.Ordinal);
8780                         if (origUrlIndex == -1)
8781                             return false;
8782
8783                         this.StatusText.Select(origUrlIndex, tmp.Length);
8784                         this.StatusText.SelectedText = result;
8785
8786                         // undoバッファにセット
8787                         undotmp.Before = tmp;
8788                         undotmp.After = result;
8789
8790                         if (this.urlUndoBuffer == null)
8791                         {
8792                             this.urlUndoBuffer = new List<UrlUndo>();
8793                             this.UrlUndoToolStripMenuItem.Enabled = true;
8794                         }
8795
8796                         this.urlUndoBuffer.Add(undotmp);
8797                     }
8798                 }
8799             }
8800             else
8801             {
8802                 const string url = @"(?<before>(?:[^\""':!=]|^|\:))" +
8803                                    @"(?<url>(?<protocol>https?://)" +
8804                                    @"(?<domain>(?:[\.-]|[^\p{P}\s])+\.[a-z]{2,}(?::[0-9]+)?)" +
8805                                    @"(?<path>/[a-z0-9!*//();:&=+$/%#\-_.,~@]*[a-z0-9)=#/]?)?" +
8806                                    @"(?<query>\?[a-z0-9!*//();:&=+$/%#\-_.,~@?]*[a-z0-9_&=#/])?)";
8807                 // 正規表現にマッチしたURL文字列をtinyurl化
8808                 foreach (Match mt in Regex.Matches(this.StatusText.Text, url, RegexOptions.IgnoreCase))
8809                 {
8810                     if (this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal) == -1)
8811                         continue;
8812                     var tmp = mt.Result("${url}");
8813                     if (tmp.StartsWith("w", StringComparison.OrdinalIgnoreCase))
8814                         tmp = "http://" + tmp;
8815                     var undotmp = new UrlUndo();
8816
8817                     // 選んだURLを選択(?)
8818                     this.StatusText.Select(this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal), mt.Result("${url}").Length);
8819
8820                     // nico.ms使用、nicovideoにマッチしたら変換
8821                     if (SettingManager.Common.Nicoms && Regex.IsMatch(tmp, nico))
8822                     {
8823                         result = Nicoms.Shorten(tmp);
8824                     }
8825                     else if (converterType != MyCommon.UrlConverter.Nicoms)
8826                     {
8827                         // 短縮URL変換
8828                         try
8829                         {
8830                             var srcUri = new Uri(tmp);
8831                             var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri);
8832                             result = resultUri.AbsoluteUri;
8833                         }
8834                         catch (HttpRequestException e)
8835                         {
8836                             // 例外のメッセージが「Response status code does not indicate success: 500 (Internal Server Error).」
8837                             // のように長いので「:」が含まれていればそれ以降のみを抽出する
8838                             var message = e.Message.Split(new[] { ':' }, count: 2).Last();
8839
8840                             this.StatusLabel.Text = converterType + ":" + message;
8841                             continue;
8842                         }
8843                         catch (WebApiException e)
8844                         {
8845                             this.StatusLabel.Text = converterType + ":" + e.Message;
8846                             continue;
8847                         }
8848                         catch (UriFormatException e)
8849                         {
8850                             this.StatusLabel.Text = converterType + ":" + e.Message;
8851                             continue;
8852                         }
8853                     }
8854                     else
8855                     {
8856                         continue;
8857                     }
8858
8859                     if (!MyCommon.IsNullOrEmpty(result))
8860                     {
8861                         // 短縮 URL が生成されるまでの間に投稿欄から元の URL が削除されていたら中断する
8862                         var origUrlIndex = this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal);
8863                         if (origUrlIndex == -1)
8864                             return false;
8865
8866                         this.StatusText.Select(origUrlIndex, mt.Result("${url}").Length);
8867                         this.StatusText.SelectedText = result;
8868                         // undoバッファにセット
8869                         undotmp.Before = mt.Result("${url}");
8870                         undotmp.After = result;
8871
8872                         if (this.urlUndoBuffer == null)
8873                         {
8874                             this.urlUndoBuffer = new List<UrlUndo>();
8875                             this.UrlUndoToolStripMenuItem.Enabled = true;
8876                         }
8877
8878                         this.urlUndoBuffer.Add(undotmp);
8879                     }
8880                 }
8881             }
8882
8883             return true;
8884         }
8885
8886         private void DoUrlUndo()
8887         {
8888             if (this.urlUndoBuffer != null)
8889             {
8890                 var tmp = this.StatusText.Text;
8891                 foreach (var data in this.urlUndoBuffer)
8892                 {
8893                     tmp = tmp.Replace(data.After, data.Before);
8894                 }
8895                 this.StatusText.Text = tmp;
8896                 this.urlUndoBuffer = null;
8897                 this.UrlUndoToolStripMenuItem.Enabled = false;
8898                 this.StatusText.SelectionStart = 0;
8899                 this.StatusText.SelectionLength = 0;
8900             }
8901         }
8902
8903         private async void TinyURLToolStripMenuItem_Click(object sender, EventArgs e)
8904             => await this.UrlConvertAsync(MyCommon.UrlConverter.TinyUrl);
8905
8906         private async void IsgdToolStripMenuItem_Click(object sender, EventArgs e)
8907             => await this.UrlConvertAsync(MyCommon.UrlConverter.Isgd);
8908
8909         private async void UxnuMenuItem_Click(object sender, EventArgs e)
8910             => await this.UrlConvertAsync(MyCommon.UrlConverter.Uxnu);
8911
8912         private async void UrlConvertAutoToolStripMenuItem_Click(object sender, EventArgs e)
8913         {
8914             if (!await this.UrlConvertAsync(SettingManager.Common.AutoShortUrlFirst))
8915             {
8916                 var rnd = new Random();
8917
8918                 MyCommon.UrlConverter svc;
8919                 // 前回使用した短縮URLサービス以外を選択する
8920                 do
8921                 {
8922                     svc = (MyCommon.UrlConverter)rnd.Next(System.Enum.GetNames(typeof(MyCommon.UrlConverter)).Length);
8923                 }
8924                 while (svc == SettingManager.Common.AutoShortUrlFirst || svc == MyCommon.UrlConverter.Nicoms || svc == MyCommon.UrlConverter.Unu);
8925                 await this.UrlConvertAsync(svc);
8926             }
8927         }
8928
8929         private void UrlUndoToolStripMenuItem_Click(object sender, EventArgs e)
8930             => this.DoUrlUndo();
8931
8932         private void NewPostPopMenuItem_CheckStateChanged(object sender, EventArgs e)
8933         {
8934             this.NotifyFileMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
8935             this.NewPostPopMenuItem.Checked = this.NotifyFileMenuItem.Checked;
8936             SettingManager.Common.NewAllPop = this.NewPostPopMenuItem.Checked;
8937             this.MarkSettingCommonModified();
8938         }
8939
8940         private void ListLockMenuItem_CheckStateChanged(object sender, EventArgs e)
8941         {
8942             this.ListLockMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
8943             this.LockListFileMenuItem.Checked = this.ListLockMenuItem.Checked;
8944             SettingManager.Common.ListLock = this.ListLockMenuItem.Checked;
8945             this.MarkSettingCommonModified();
8946         }
8947
8948         private void MenuStrip1_MenuActivate(object sender, EventArgs e)
8949         {
8950             // フォーカスがメニューに移る (MenuStrip1.Tag フラグを立てる)
8951             this.MenuStrip1.Tag = new object();
8952             this.MenuStrip1.Select(); // StatusText がフォーカスを持っている場合 Leave が発生
8953         }
8954
8955         private void MenuStrip1_MenuDeactivate(object sender, EventArgs e)
8956         {
8957             var currentTabPage = this.CurrentTabPage;
8958             if (this.Tag != null) // 設定された戻り先へ遷移
8959             {
8960                 if (this.Tag == currentTabPage)
8961                     ((Control)currentTabPage.Tag).Select();
8962                 else
8963                     ((Control)this.Tag).Select();
8964             }
8965             else // 戻り先が指定されていない (初期状態) 場合はタブに遷移
8966             {
8967                 this.Tag = currentTabPage.Tag;
8968                 ((Control)this.Tag).Select();
8969             }
8970             // フォーカスがメニューに遷移したかどうかを表すフラグを降ろす
8971             this.MenuStrip1.Tag = null;
8972         }
8973
8974         private void MyList_ColumnReordered(object sender, ColumnReorderedEventArgs e)
8975         {
8976             var lst = (DetailsListView)sender;
8977             if (SettingManager.Local == null) return;
8978
8979             if (this.iconCol)
8980             {
8981                 SettingManager.Local.Width1 = lst.Columns[0].Width;
8982                 SettingManager.Local.Width3 = lst.Columns[1].Width;
8983             }
8984             else
8985             {
8986                 var darr = new int[lst.Columns.Count];
8987                 for (var i = 0; i < lst.Columns.Count; i++)
8988                 {
8989                     darr[lst.Columns[i].DisplayIndex] = i;
8990                 }
8991                 MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex);
8992
8993                 for (var i = 0; i < lst.Columns.Count; i++)
8994                 {
8995                     switch (darr[i])
8996                     {
8997                         case 0:
8998                             SettingManager.Local.DisplayIndex1 = i;
8999                             break;
9000                         case 1:
9001                             SettingManager.Local.DisplayIndex2 = i;
9002                             break;
9003                         case 2:
9004                             SettingManager.Local.DisplayIndex3 = i;
9005                             break;
9006                         case 3:
9007                             SettingManager.Local.DisplayIndex4 = i;
9008                             break;
9009                         case 4:
9010                             SettingManager.Local.DisplayIndex5 = i;
9011                             break;
9012                         case 5:
9013                             SettingManager.Local.DisplayIndex6 = i;
9014                             break;
9015                         case 6:
9016                             SettingManager.Local.DisplayIndex7 = i;
9017                             break;
9018                         case 7:
9019                             SettingManager.Local.DisplayIndex8 = i;
9020                             break;
9021                     }
9022                 }
9023                 SettingManager.Local.Width1 = lst.Columns[0].Width;
9024                 SettingManager.Local.Width2 = lst.Columns[1].Width;
9025                 SettingManager.Local.Width3 = lst.Columns[2].Width;
9026                 SettingManager.Local.Width4 = lst.Columns[3].Width;
9027                 SettingManager.Local.Width5 = lst.Columns[4].Width;
9028                 SettingManager.Local.Width6 = lst.Columns[5].Width;
9029                 SettingManager.Local.Width7 = lst.Columns[6].Width;
9030                 SettingManager.Local.Width8 = lst.Columns[7].Width;
9031             }
9032             this.MarkSettingLocalModified();
9033             this.isColumnChanged = true;
9034         }
9035
9036         private void MyList_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e)
9037         {
9038             var lst = (DetailsListView)sender;
9039             if (SettingManager.Local == null) return;
9040
9041             var modified = false;
9042             if (this.iconCol)
9043             {
9044                 if (SettingManager.Local.Width1 != lst.Columns[0].Width)
9045                 {
9046                     SettingManager.Local.Width1 = lst.Columns[0].Width;
9047                     modified = true;
9048                 }
9049                 if (SettingManager.Local.Width3 != lst.Columns[1].Width)
9050                 {
9051                     SettingManager.Local.Width3 = lst.Columns[1].Width;
9052                     modified = true;
9053                 }
9054             }
9055             else
9056             {
9057                 if (SettingManager.Local.Width1 != lst.Columns[0].Width)
9058                 {
9059                     SettingManager.Local.Width1 = lst.Columns[0].Width;
9060                     modified = true;
9061                 }
9062                 if (SettingManager.Local.Width2 != lst.Columns[1].Width)
9063                 {
9064                     SettingManager.Local.Width2 = lst.Columns[1].Width;
9065                     modified = true;
9066                 }
9067                 if (SettingManager.Local.Width3 != lst.Columns[2].Width)
9068                 {
9069                     SettingManager.Local.Width3 = lst.Columns[2].Width;
9070                     modified = true;
9071                 }
9072                 if (SettingManager.Local.Width4 != lst.Columns[3].Width)
9073                 {
9074                     SettingManager.Local.Width4 = lst.Columns[3].Width;
9075                     modified = true;
9076                 }
9077                 if (SettingManager.Local.Width5 != lst.Columns[4].Width)
9078                 {
9079                     SettingManager.Local.Width5 = lst.Columns[4].Width;
9080                     modified = true;
9081                 }
9082                 if (SettingManager.Local.Width6 != lst.Columns[5].Width)
9083                 {
9084                     SettingManager.Local.Width6 = lst.Columns[5].Width;
9085                     modified = true;
9086                 }
9087                 if (SettingManager.Local.Width7 != lst.Columns[6].Width)
9088                 {
9089                     SettingManager.Local.Width7 = lst.Columns[6].Width;
9090                     modified = true;
9091                 }
9092                 if (SettingManager.Local.Width8 != lst.Columns[7].Width)
9093                 {
9094                     SettingManager.Local.Width8 = lst.Columns[7].Width;
9095                     modified = true;
9096                 }
9097             }
9098             if (modified)
9099             {
9100                 this.MarkSettingLocalModified();
9101                 this.isColumnChanged = true;
9102             }
9103         }
9104
9105         private void SplitContainer2_SplitterMoved(object sender, SplitterEventArgs e)
9106         {
9107             if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height;
9108             this.MarkSettingLocalModified();
9109         }
9110
9111         private void TweenMain_DragDrop(object sender, DragEventArgs e)
9112         {
9113             if (e.Data.GetDataPresent(DataFormats.FileDrop))
9114             {
9115                 if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く
9116                 {
9117                     this.SelectMedia_DragDrop(e);
9118                 }
9119             }
9120             else if (e.Data.GetDataPresent("UniformResourceLocatorW"))
9121             {
9122                 var (url, title) = GetUrlFromDataObject(e.Data);
9123
9124                 string appendText;
9125                 if (title == null)
9126                     appendText = url;
9127                 else
9128                     appendText = title + " " + url;
9129
9130                 if (this.StatusText.TextLength == 0)
9131                     this.StatusText.Text = appendText;
9132                 else
9133                     this.StatusText.Text += " " + appendText;
9134             }
9135             else if (e.Data.GetDataPresent(DataFormats.UnicodeText))
9136             {
9137                 var text = (string)e.Data.GetData(DataFormats.UnicodeText);
9138                 if (text != null)
9139                     this.StatusText.Text += text;
9140             }
9141             else if (e.Data.GetDataPresent(DataFormats.StringFormat))
9142             {
9143                 var data = (string)e.Data.GetData(DataFormats.StringFormat, true);
9144                 if (data != null) this.StatusText.Text += data;
9145             }
9146         }
9147
9148         /// <summary>
9149         /// IDataObject から URL とタイトルの対を取得します
9150         /// </summary>
9151         /// <remarks>
9152         /// タイトルのみ取得できなかった場合は Value2 が null のタプルを返すことがあります。
9153         /// </remarks>
9154         /// <exception cref="ArgumentException">不正なフォーマットが入力された場合</exception>
9155         /// <exception cref="NotSupportedException">サポートされていないデータが入力された場合</exception>
9156         internal static (string Url, string? Title) GetUrlFromDataObject(IDataObject data)
9157         {
9158             if (data.GetDataPresent("text/x-moz-url"))
9159             {
9160                 // Firefox, Google Chrome で利用可能
9161                 // 参照: https://developer.mozilla.org/ja/docs/DragDrop/Recommended_Drag_Types
9162
9163                 using var stream = (MemoryStream)data.GetData("text/x-moz-url");
9164                 var lines = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0').Split('\n');
9165                 if (lines.Length < 2)
9166                     throw new ArgumentException("不正な text/x-moz-url フォーマットです", nameof(data));
9167
9168                 return (lines[0], lines[1]);
9169             }
9170             else if (data.GetDataPresent("IESiteModeToUrl"))
9171             {
9172                 // Internet Exproler 用
9173                 // 保護モードが有効なデフォルトの IE では DragDrop イベントが発火しないため使えない
9174
9175                 using var stream = (MemoryStream)data.GetData("IESiteModeToUrl");
9176                 var lines = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0').Split('\0');
9177                 if (lines.Length < 2)
9178                     throw new ArgumentException("不正な IESiteModeToUrl フォーマットです", nameof(data));
9179
9180                 return (lines[0], lines[1]);
9181             }
9182             else if (data.GetDataPresent("UniformResourceLocatorW"))
9183             {
9184                 // それ以外のブラウザ向け
9185
9186                 using var stream = (MemoryStream)data.GetData("UniformResourceLocatorW");
9187                 var url = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0');
9188                 return (url, null);
9189             }
9190
9191             throw new NotSupportedException("サポートされていないデータ形式です: " + data.GetFormats()[0]);
9192         }
9193
9194         private void TweenMain_DragEnter(object sender, DragEventArgs e)
9195         {
9196             if (e.Data.GetDataPresent(DataFormats.FileDrop))
9197             {
9198                 if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く
9199                 {
9200                     this.SelectMedia_DragEnter(e);
9201                     return;
9202                 }
9203             }
9204             else if (e.Data.GetDataPresent("UniformResourceLocatorW"))
9205             {
9206                 e.Effect = DragDropEffects.Copy;
9207                 return;
9208             }
9209             else if (e.Data.GetDataPresent(DataFormats.UnicodeText))
9210             {
9211                 e.Effect = DragDropEffects.Copy;
9212                 return;
9213             }
9214             else if (e.Data.GetDataPresent(DataFormats.StringFormat))
9215             {
9216                 e.Effect = DragDropEffects.Copy;
9217                 return;
9218             }
9219
9220             e.Effect = DragDropEffects.None;
9221         }
9222
9223         private void TweenMain_DragOver(object sender, DragEventArgs e)
9224         {
9225         }
9226
9227         public bool IsNetworkAvailable()
9228         {
9229             var nw = MyCommon.IsNetworkAvailable();
9230             this.myStatusOnline = nw;
9231             return nw;
9232         }
9233
9234         public async Task OpenUriAsync(Uri uri, bool isReverseSettings = false)
9235         {
9236             var uriStr = uri.AbsoluteUri;
9237
9238             // OpenTween 内部で使用する URL
9239             if (uri.Authority == "opentween")
9240             {
9241                 await this.OpenInternalUriAsync(uri);
9242                 return;
9243             }
9244
9245             // ハッシュタグを含む Twitter 検索
9246             if (uri.Host == "twitter.com" && uri.AbsolutePath == "/search" && uri.Query.Contains("q=%23"))
9247             {
9248                 // ハッシュタグの場合は、タブで開く
9249                 var unescapedQuery = Uri.UnescapeDataString(uri.Query);
9250                 var pos = unescapedQuery.IndexOf('#');
9251                 if (pos == -1) return;
9252
9253                 var hash = unescapedQuery.Substring(pos);
9254                 this.HashSupl.AddItem(hash);
9255                 this.HashMgr.AddHashToHistory(hash.Trim(), false);
9256                 this.AddNewTabForSearch(hash);
9257                 return;
9258             }
9259
9260             // ユーザープロフィールURL
9261             // フラグが立っている場合は設定と逆の動作をする
9262             if (SettingManager.Common.OpenUserTimeline && !isReverseSettings ||
9263                 !SettingManager.Common.OpenUserTimeline && isReverseSettings)
9264             {
9265                 var userUriMatch = Regex.Match(uriStr, "^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)$");
9266                 if (userUriMatch.Success)
9267                 {
9268                     var screenName = userUriMatch.Groups["ScreenName"].Value;
9269                     if (this.IsTwitterId(screenName))
9270                     {
9271                         await this.AddNewTabForUserTimeline(screenName);
9272                         return;
9273                     }
9274                 }
9275             }
9276
9277             // どのパターンにも該当しないURL
9278             await MyCommon.OpenInBrowserAsync(this, uriStr);
9279         }
9280
9281         /// <summary>
9282         /// OpenTween 内部の機能を呼び出すための URL を開きます
9283         /// </summary>
9284         private async Task OpenInternalUriAsync(Uri uri)
9285         {
9286             // ツイートを開く (//opentween/status/:status_id)
9287             var match = Regex.Match(uri.AbsolutePath, @"^/status/(\d+)$");
9288             if (match.Success)
9289             {
9290                 var statusId = long.Parse(match.Groups[1].Value);
9291                 await this.OpenRelatedTab(statusId);
9292                 return;
9293             }
9294         }
9295
9296         private void ListTabSelect(TabPage tab)
9297         {
9298             this.SetListProperty();
9299
9300             this.PurgeListViewItemCache();
9301
9302             this.statuses.SelectTab(tab.Text);
9303
9304             var listView = this.CurrentListView;
9305
9306             this.anchorPost = null;
9307             this.anchorFlag = false;
9308
9309             if (this.iconCol)
9310             {
9311                 listView.Columns[1].Text = this.columnText[2];
9312             }
9313             else
9314             {
9315                 for (var i = 0; i < listView.Columns.Count; i++)
9316                 {
9317                     listView.Columns[i].Text = this.columnText[i];
9318                 }
9319             }
9320         }
9321
9322         private void ListTab_Selecting(object sender, TabControlCancelEventArgs e)
9323             => this.ListTabSelect(e.TabPage);
9324
9325         private void SelectListItem(DetailsListView lView, int index)
9326         {
9327             // 単一
9328             var bnd = new Rectangle();
9329             var flg = false;
9330             var item = lView.FocusedItem;
9331             if (item != null)
9332             {
9333                 bnd = item.Bounds;
9334                 flg = true;
9335             }
9336
9337             do
9338             {
9339                 lView.SelectedIndices.Clear();
9340             }
9341             while (lView.SelectedIndices.Count > 0);
9342             item = lView.Items[index];
9343             item.Selected = true;
9344             item.Focused = true;
9345
9346             if (flg) lView.Invalidate(bnd);
9347         }
9348
9349         private void SelectListItem(DetailsListView lView, int[]? index, int focusedIndex, int selectionMarkIndex)
9350         {
9351             // 複数
9352             var bnd = new Rectangle();
9353             var flg = false;
9354             var item = lView.FocusedItem;
9355             if (item != null)
9356             {
9357                 bnd = item.Bounds;
9358                 flg = true;
9359             }
9360
9361             if (index != null)
9362             {
9363                 lView.SelectItems(index);
9364             }
9365             if (selectionMarkIndex > -1 && lView.VirtualListSize > selectionMarkIndex)
9366             {
9367                 lView.SelectionMark = selectionMarkIndex;
9368             }
9369             if (focusedIndex > -1 && lView.VirtualListSize > focusedIndex)
9370             {
9371                 lView.Items[focusedIndex].Focused = true;
9372             }
9373             else if (index != null && index.Length != 0)
9374             {
9375                 lView.Items[index.Last()].Focused = true;
9376             }
9377
9378             if (flg) lView.Invalidate(bnd);
9379         }
9380
9381         private async void TweenMain_Shown(object sender, EventArgs e)
9382         {
9383             this.NotifyIcon1.Visible = true;
9384
9385             if (this.IsNetworkAvailable())
9386             {
9387                 var loadTasks = new List<Task>
9388                 {
9389                     this.RefreshMuteUserIdsAsync(),
9390                     this.RefreshBlockIdsAsync(),
9391                     this.RefreshNoRetweetIdsAsync(),
9392                     this.RefreshTwitterConfigurationAsync(),
9393                     this.RefreshTabAsync<HomeTabModel>(),
9394                     this.RefreshTabAsync<MentionsTabModel>(),
9395                     this.RefreshTabAsync<DirectMessagesTabModel>(),
9396                     this.RefreshTabAsync<PublicSearchTabModel>(),
9397                     this.RefreshTabAsync<UserTimelineTabModel>(),
9398                     this.RefreshTabAsync<ListTimelineTabModel>(),
9399                 };
9400
9401                 if (SettingManager.Common.StartupFollowers)
9402                     loadTasks.Add(this.RefreshFollowerIdsAsync());
9403
9404                 if (SettingManager.Common.GetFav)
9405                     loadTasks.Add(this.RefreshTabAsync<FavoritesTabModel>());
9406
9407                 var allTasks = Task.WhenAll(loadTasks);
9408
9409                 var i = 0;
9410                 while (true)
9411                 {
9412                     var timeout = Task.Delay(5000);
9413                     if (await Task.WhenAny(allTasks, timeout) != timeout)
9414                         break;
9415
9416                     i += 1;
9417                     if (i > 24) break; // 120秒間初期処理が終了しなかったら強制的に打ち切る
9418
9419                     if (MyCommon.EndingFlag)
9420                         return;
9421                 }
9422
9423                 if (MyCommon.EndingFlag) return;
9424
9425                 if (ApplicationSettings.VersionInfoUrl != null)
9426                 {
9427                     // バージョンチェック(引数:起動時チェックの場合はtrue・・・チェック結果のメッセージを表示しない)
9428                     if (SettingManager.Common.StartupVersion)
9429                         await this.CheckNewVersion(true);
9430                 }
9431                 else
9432                 {
9433                     // ApplicationSetting.cs の設定により更新チェックが無効化されている場合
9434                     this.VerUpMenuItem.Enabled = false;
9435                     this.VerUpMenuItem.Available = false;
9436                     this.ToolStripSeparator16.Available = false; // VerUpMenuItem の一つ上にあるセパレータ
9437                 }
9438
9439                 // 権限チェック read/write権限(xAuthで取得したトークン)の場合は再認証を促す
9440                 if (MyCommon.TwitterApiInfo.AccessLevel == TwitterApiAccessLevel.ReadWrite)
9441                 {
9442                     MessageBox.Show(Properties.Resources.ReAuthorizeText);
9443                     this.SettingStripMenuItem_Click(this.SettingStripMenuItem, EventArgs.Empty);
9444                 }
9445
9446                 // 取得失敗の場合は再試行する
9447                 var reloadTasks = new List<Task>();
9448
9449                 if (!this.tw.GetFollowersSuccess && SettingManager.Common.StartupFollowers)
9450                     reloadTasks.Add(this.RefreshFollowerIdsAsync());
9451
9452                 if (!this.tw.GetNoRetweetSuccess)
9453                     reloadTasks.Add(this.RefreshNoRetweetIdsAsync());
9454
9455                 if (this.tw.Configuration.PhotoSizeLimit == 0)
9456                     reloadTasks.Add(this.RefreshTwitterConfigurationAsync());
9457
9458                 await Task.WhenAll(reloadTasks);
9459             }
9460
9461             this.initial = false;
9462
9463             this.timelineScheduler.Enabled = true;
9464         }
9465
9466         private async Task DoGetFollowersMenu()
9467         {
9468             await this.RefreshFollowerIdsAsync();
9469             this.DispSelectedPost(true);
9470         }
9471
9472         private async void GetFollowersAllToolStripMenuItem_Click(object sender, EventArgs e)
9473             => await this.DoGetFollowersMenu();
9474
9475         private void ReTweetUnofficialStripMenuItem_Click(object sender, EventArgs e)
9476             => this.DoReTweetUnofficial();
9477
9478         private async Task DoReTweetOfficial(bool isConfirm)
9479         {
9480             // 公式RT
9481             if (this.ExistCurrentPost)
9482             {
9483                 var selectedPosts = this.CurrentTab.SelectedPosts;
9484
9485                 if (selectedPosts.Any(x => !x.CanRetweetBy(this.twitterApi.CurrentUserId)))
9486                 {
9487                     if (selectedPosts.Any(x => x.IsProtect))
9488                         MessageBox.Show("Protected.");
9489
9490                     this.doFavRetweetFlags = false;
9491                     return;
9492                 }
9493
9494                 if (selectedPosts.Length > 15)
9495                 {
9496                     MessageBox.Show(Properties.Resources.RetweetLimitText);
9497                     this.doFavRetweetFlags = false;
9498                     return;
9499                 }
9500                 else if (selectedPosts.Length > 1)
9501                 {
9502                     var questionText = Properties.Resources.RetweetQuestion2;
9503                     if (this.doFavRetweetFlags) questionText = Properties.Resources.FavoriteRetweetQuestionText1;
9504                     switch (MessageBox.Show(questionText, "Retweet", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question))
9505                     {
9506                         case DialogResult.Cancel:
9507                         case DialogResult.No:
9508                             this.doFavRetweetFlags = false;
9509                             return;
9510                     }
9511                 }
9512                 else
9513                 {
9514                     if (!SettingManager.Common.RetweetNoConfirm)
9515                     {
9516                         var questiontext = Properties.Resources.RetweetQuestion1;
9517                         if (this.doFavRetweetFlags) questiontext = Properties.Resources.FavoritesRetweetQuestionText2;
9518                         if (isConfirm && MessageBox.Show(questiontext, "Retweet", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
9519                         {
9520                             this.doFavRetweetFlags = false;
9521                             return;
9522                         }
9523                     }
9524                 }
9525
9526                 var statusIds = selectedPosts.Select(x => x.StatusId).ToList();
9527
9528                 await this.RetweetAsync(statusIds);
9529             }
9530         }
9531
9532         private async void ReTweetStripMenuItem_Click(object sender, EventArgs e)
9533             => await this.DoReTweetOfficial(true);
9534
9535         private async Task FavoritesRetweetOfficial()
9536         {
9537             if (!this.ExistCurrentPost) return;
9538             this.doFavRetweetFlags = true;
9539             var retweetTask = this.DoReTweetOfficial(true);
9540             if (this.doFavRetweetFlags)
9541             {
9542                 this.doFavRetweetFlags = false;
9543                 var favoriteTask = this.FavoriteChange(true, false);
9544
9545                 await Task.WhenAll(retweetTask, favoriteTask);
9546             }
9547             else
9548             {
9549                 await retweetTask;
9550             }
9551         }
9552
9553         private async Task FavoritesRetweetUnofficial()
9554         {
9555             var post = this.CurrentPost;
9556             if (this.ExistCurrentPost && post != null && !post.IsDm)
9557             {
9558                 this.doFavRetweetFlags = true;
9559                 var favoriteTask = this.FavoriteChange(true);
9560                 if (!post.IsProtect && this.doFavRetweetFlags)
9561                 {
9562                     this.doFavRetweetFlags = false;
9563                     this.DoReTweetUnofficial();
9564                 }
9565
9566                 await favoriteTask;
9567             }
9568         }
9569
9570         /// <summary>
9571         /// TweetFormatterクラスによって整形された状態のHTMLを、非公式RT用に元のツイートに復元します
9572         /// </summary>
9573         /// <param name="statusHtml">TweetFormatterによって整形された状態のHTML</param>
9574         /// <param name="multiline">trueであればBRタグを改行に、falseであればスペースに変換します</param>
9575         /// <returns>復元されたツイート本文</returns>
9576         internal static string CreateRetweetUnofficial(string statusHtml, bool multiline)
9577         {
9578             // TweetFormatterクラスによって整形された状態のHTMLを元のツイートに復元します
9579
9580             // 通常の URL
9581             statusHtml = Regex.Replace(statusHtml, "<a href=\"(?<href>.+?)\" title=\"(?<title>.+?)\">(?<text>.+?)</a>", "${title}");
9582             // メンション
9583             statusHtml = Regex.Replace(statusHtml, "<a class=\"mention\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
9584             // ハッシュタグ
9585             statusHtml = Regex.Replace(statusHtml, "<a class=\"hashtag\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
9586             // 絵文字
9587             statusHtml = Regex.Replace(statusHtml, "<img class=\"emoji\" src=\".+?\" alt=\"(?<text>.+?)\" />", "${text}");
9588
9589             // <br> 除去
9590             if (multiline)
9591                 statusHtml = statusHtml.Replace("<br>", Environment.NewLine);
9592             else
9593                 statusHtml = statusHtml.Replace("<br>", " ");
9594
9595             // &nbsp; は本来であれば U+00A0 (NON-BREAK SPACE) に置換すべきですが、
9596             // 現状では半角スペースの代用として &nbsp; を使用しているため U+0020 に置換します
9597             statusHtml = statusHtml.Replace("&nbsp;", " ");
9598
9599             return WebUtility.HtmlDecode(statusHtml);
9600         }
9601
9602         private void DumpPostClassToolStripMenuItem_Click(object sender, EventArgs e)
9603         {
9604             this.tweetDetailsView.DumpPostClass = this.DumpPostClassToolStripMenuItem.Checked;
9605
9606             if (this.CurrentPost != null)
9607                 this.DispSelectedPost(true);
9608         }
9609
9610         private void MenuItemHelp_DropDownOpening(object sender, EventArgs e)
9611         {
9612             if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock, Keys.Control, Keys.Shift))
9613                 this.DebugModeToolStripMenuItem.Visible = true;
9614             else
9615                 this.DebugModeToolStripMenuItem.Visible = false;
9616         }
9617
9618         private void UrlMultibyteSplitMenuItem_CheckedChanged(object sender, EventArgs e)
9619             => this.urlMultibyteSplit = ((ToolStripMenuItem)sender).Checked;
9620
9621         private void PreventSmsCommandMenuItem_CheckedChanged(object sender, EventArgs e)
9622             => this.preventSmsCommand = ((ToolStripMenuItem)sender).Checked;
9623
9624         private void UrlAutoShortenMenuItem_CheckedChanged(object sender, EventArgs e)
9625             => SettingManager.Common.UrlConvertAuto = ((ToolStripMenuItem)sender).Checked;
9626
9627         private void IdeographicSpaceToSpaceMenuItem_Click(object sender, EventArgs e)
9628         {
9629             SettingManager.Common.WideSpaceConvert = ((ToolStripMenuItem)sender).Checked;
9630             this.MarkSettingCommonModified();
9631         }
9632
9633         private void FocusLockMenuItem_CheckedChanged(object sender, EventArgs e)
9634         {
9635             SettingManager.Common.FocusLockToStatusText = ((ToolStripMenuItem)sender).Checked;
9636             this.MarkSettingCommonModified();
9637         }
9638
9639         private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e)
9640         {
9641             this.UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit;
9642             this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand;
9643             this.UrlAutoShortenMenuItem.Checked = SettingManager.Common.UrlConvertAuto;
9644             this.IdeographicSpaceToSpaceMenuItem.Checked = SettingManager.Common.WideSpaceConvert;
9645             this.MultiLineMenuItem.Checked = SettingManager.Local.StatusMultiline;
9646             this.FocusLockMenuItem.Checked = SettingManager.Common.FocusLockToStatusText;
9647         }
9648
9649         private void ContextMenuPostMode_Opening(object sender, CancelEventArgs e)
9650         {
9651             this.UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit;
9652             this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand;
9653             this.UrlAutoShortenPullDownMenuItem.Checked = SettingManager.Common.UrlConvertAuto;
9654             this.IdeographicSpaceToSpacePullDownMenuItem.Checked = SettingManager.Common.WideSpaceConvert;
9655             this.MultiLinePullDownMenuItem.Checked = SettingManager.Local.StatusMultiline;
9656             this.FocusLockPullDownMenuItem.Checked = SettingManager.Common.FocusLockToStatusText;
9657         }
9658
9659         private void TraceOutToolStripMenuItem_Click(object sender, EventArgs e)
9660         {
9661             if (this.TraceOutToolStripMenuItem.Checked)
9662                 MyCommon.TraceFlag = true;
9663             else
9664                 MyCommon.TraceFlag = false;
9665         }
9666
9667         private void TweenMain_Deactivate(object sender, EventArgs e)
9668             => this.StatusText_Leave(this.StatusText, EventArgs.Empty); // 画面が非アクティブになったら、発言欄の背景色をデフォルトへ
9669
9670         private void TabRenameMenuItem_Click(object sender, EventArgs e)
9671         {
9672             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
9673
9674             _ = this.TabRename(this.rclickTabName, out _);
9675         }
9676
9677         private async void BitlyToolStripMenuItem_Click(object sender, EventArgs e)
9678             => await this.UrlConvertAsync(MyCommon.UrlConverter.Bitly);
9679
9680         private async void JmpToolStripMenuItem_Click(object sender, EventArgs e)
9681             => await this.UrlConvertAsync(MyCommon.UrlConverter.Jmp);
9682
9683         private async void ApiUsageInfoMenuItem_Click(object sender, EventArgs e)
9684         {
9685             TwitterApiStatus? apiStatus;
9686
9687             using (var dialog = new WaitingDialog(Properties.Resources.ApiInfo6))
9688             {
9689                 var cancellationToken = dialog.EnableCancellation();
9690
9691                 try
9692                 {
9693                     var task = this.tw.GetInfoApi();
9694                     apiStatus = await dialog.WaitForAsync(this, task);
9695                 }
9696                 catch (WebApiException)
9697                 {
9698                     apiStatus = null;
9699                 }
9700
9701                 if (cancellationToken.IsCancellationRequested)
9702                     return;
9703
9704                 if (apiStatus == null)
9705                 {
9706                     MessageBox.Show(Properties.Resources.ApiInfo5, Properties.Resources.ApiInfo4, MessageBoxButtons.OK, MessageBoxIcon.Information);
9707                     return;
9708                 }
9709             }
9710
9711             using var apiDlg = new ApiInfoDialog();
9712             apiDlg.ShowDialog(this);
9713         }
9714
9715         private async void FollowCommandMenuItem_Click(object sender, EventArgs e)
9716         {
9717             var id = this.CurrentPost?.ScreenName ?? "";
9718
9719             await this.FollowCommand(id);
9720         }
9721
9722         internal async Task FollowCommand(string id)
9723         {
9724             using (var inputName = new InputTabName())
9725             {
9726                 inputName.FormTitle = "Follow";
9727                 inputName.FormDescription = Properties.Resources.FRMessage1;
9728                 inputName.TabName = id;
9729
9730                 if (inputName.ShowDialog(this) != DialogResult.OK)
9731                     return;
9732                 if (string.IsNullOrWhiteSpace(inputName.TabName))
9733                     return;
9734
9735                 id = inputName.TabName.Trim();
9736             }
9737
9738             using (var dialog = new WaitingDialog(Properties.Resources.FollowCommandText1))
9739             {
9740                 try
9741                 {
9742                     var task = this.twitterApi.FriendshipsCreate(id).IgnoreResponse();
9743                     await dialog.WaitForAsync(this, task);
9744                 }
9745                 catch (WebApiException ex)
9746                 {
9747                     MessageBox.Show(Properties.Resources.FRMessage2 + ex.Message);
9748                     return;
9749                 }
9750             }
9751
9752             MessageBox.Show(Properties.Resources.FRMessage3);
9753         }
9754
9755         private async void RemoveCommandMenuItem_Click(object sender, EventArgs e)
9756         {
9757             var id = this.CurrentPost?.ScreenName ?? "";
9758
9759             await this.RemoveCommand(id, false);
9760         }
9761
9762         internal async Task RemoveCommand(string id, bool skipInput)
9763         {
9764             if (!skipInput)
9765             {
9766                 using var inputName = new InputTabName();
9767                 inputName.FormTitle = "Unfollow";
9768                 inputName.FormDescription = Properties.Resources.FRMessage1;
9769                 inputName.TabName = id;
9770
9771                 if (inputName.ShowDialog(this) != DialogResult.OK)
9772                     return;
9773                 if (string.IsNullOrWhiteSpace(inputName.TabName))
9774                     return;
9775
9776                 id = inputName.TabName.Trim();
9777             }
9778
9779             using (var dialog = new WaitingDialog(Properties.Resources.RemoveCommandText1))
9780             {
9781                 try
9782                 {
9783                     var task = this.twitterApi.FriendshipsDestroy(id).IgnoreResponse();
9784                     await dialog.WaitForAsync(this, task);
9785                 }
9786                 catch (WebApiException ex)
9787                 {
9788                     MessageBox.Show(Properties.Resources.FRMessage2 + ex.Message);
9789                     return;
9790                 }
9791             }
9792
9793             MessageBox.Show(Properties.Resources.FRMessage3);
9794         }
9795
9796         private async void FriendshipMenuItem_Click(object sender, EventArgs e)
9797         {
9798             var id = this.CurrentPost?.ScreenName ?? "";
9799
9800             await this.ShowFriendship(id);
9801         }
9802
9803         internal async Task ShowFriendship(string id)
9804         {
9805             using (var inputName = new InputTabName())
9806             {
9807                 inputName.FormTitle = "Show Friendships";
9808                 inputName.FormDescription = Properties.Resources.FRMessage1;
9809                 inputName.TabName = id;
9810
9811                 if (inputName.ShowDialog(this) != DialogResult.OK)
9812                     return;
9813                 if (string.IsNullOrWhiteSpace(inputName.TabName))
9814                     return;
9815
9816                 id = inputName.TabName.Trim();
9817             }
9818
9819             bool isFollowing, isFollowed;
9820
9821             using (var dialog = new WaitingDialog(Properties.Resources.ShowFriendshipText1))
9822             {
9823                 var cancellationToken = dialog.EnableCancellation();
9824
9825                 try
9826                 {
9827                     var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id);
9828                     var friendship = await dialog.WaitForAsync(this, task);
9829
9830                     isFollowing = friendship.Relationship.Source.Following;
9831                     isFollowed = friendship.Relationship.Source.FollowedBy;
9832                 }
9833                 catch (WebApiException ex)
9834                 {
9835                     if (!cancellationToken.IsCancellationRequested)
9836                         MessageBox.Show($"Err:{ex.Message}(FriendshipsShow)");
9837                     return;
9838                 }
9839
9840                 if (cancellationToken.IsCancellationRequested)
9841                     return;
9842             }
9843
9844             string result;
9845             if (isFollowing)
9846             {
9847                 result = Properties.Resources.GetFriendshipInfo1 + System.Environment.NewLine;
9848             }
9849             else
9850             {
9851                 result = Properties.Resources.GetFriendshipInfo2 + System.Environment.NewLine;
9852             }
9853             if (isFollowed)
9854             {
9855                 result += Properties.Resources.GetFriendshipInfo3;
9856             }
9857             else
9858             {
9859                 result += Properties.Resources.GetFriendshipInfo4;
9860             }
9861             result = id + Properties.Resources.GetFriendshipInfo5 + System.Environment.NewLine + result;
9862             MessageBox.Show(result);
9863         }
9864
9865         internal async Task ShowFriendship(string[] ids)
9866         {
9867             foreach (var id in ids)
9868             {
9869                 bool isFollowing, isFollowed;
9870
9871                 using (var dialog = new WaitingDialog(Properties.Resources.ShowFriendshipText1))
9872                 {
9873                     var cancellationToken = dialog.EnableCancellation();
9874
9875                     try
9876                     {
9877                         var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id);
9878                         var friendship = await dialog.WaitForAsync(this, task);
9879
9880                         isFollowing = friendship.Relationship.Source.Following;
9881                         isFollowed = friendship.Relationship.Source.FollowedBy;
9882                     }
9883                     catch (WebApiException ex)
9884                     {
9885                         if (!cancellationToken.IsCancellationRequested)
9886                             MessageBox.Show($"Err:{ex.Message}(FriendshipsShow)");
9887                         return;
9888                     }
9889
9890                     if (cancellationToken.IsCancellationRequested)
9891                         return;
9892                 }
9893
9894                 var result = "";
9895                 var ff = "";
9896
9897                 ff = "  ";
9898                 if (isFollowing)
9899                 {
9900                     ff += Properties.Resources.GetFriendshipInfo1;
9901                 }
9902                 else
9903                 {
9904                     ff += Properties.Resources.GetFriendshipInfo2;
9905                 }
9906
9907                 ff += System.Environment.NewLine + "  ";
9908                 if (isFollowed)
9909                 {
9910                     ff += Properties.Resources.GetFriendshipInfo3;
9911                 }
9912                 else
9913                 {
9914                     ff += Properties.Resources.GetFriendshipInfo4;
9915                 }
9916                 result += id + Properties.Resources.GetFriendshipInfo5 + System.Environment.NewLine + ff;
9917                 if (isFollowing)
9918                 {
9919                     if (MessageBox.Show(
9920                         Properties.Resources.GetFriendshipInfo7 + System.Environment.NewLine + result,
9921                         Properties.Resources.GetFriendshipInfo8,
9922                         MessageBoxButtons.YesNo,
9923                         MessageBoxIcon.Question,
9924                         MessageBoxDefaultButton.Button2) == DialogResult.Yes)
9925                     {
9926                         await this.RemoveCommand(id, true);
9927                     }
9928                 }
9929                 else
9930                 {
9931                     MessageBox.Show(result);
9932                 }
9933             }
9934         }
9935
9936         private async void OwnStatusMenuItem_Click(object sender, EventArgs e)
9937             => await this.DoShowUserStatus(this.tw.Username, false);
9938
9939         // TwitterIDでない固定文字列を調べる(文字列検証のみ 実際に取得はしない)
9940         // URLから切り出した文字列を渡す
9941
9942         public bool IsTwitterId(string name)
9943         {
9944             if (this.tw.Configuration.NonUsernamePaths == null || this.tw.Configuration.NonUsernamePaths.Length == 0)
9945                 return !Regex.Match(name, @"^(about|jobs|tos|privacy|who_to_follow|download|messages)$", RegexOptions.IgnoreCase).Success;
9946             else
9947                 return !this.tw.Configuration.NonUsernamePaths.Contains(name, StringComparer.InvariantCultureIgnoreCase);
9948         }
9949
9950         private void DoQuoteOfficial()
9951         {
9952             var post = this.CurrentPost;
9953             if (this.ExistCurrentPost && post != null)
9954             {
9955                 if (post.IsDm || !this.StatusText.Enabled)
9956                     return;
9957
9958                 if (post.IsProtect)
9959                 {
9960                     MessageBox.Show("Protected.");
9961                     return;
9962                 }
9963
9964                 var selection = (this.StatusText.SelectionStart, this.StatusText.SelectionLength);
9965
9966                 this.inReplyTo = null;
9967
9968                 this.StatusText.Text += " " + MyCommon.GetStatusUrl(post);
9969
9970                 (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection;
9971                 this.StatusText.Focus();
9972             }
9973         }
9974
9975         private void DoReTweetUnofficial()
9976         {
9977             // RT @id:内容
9978             var post = this.CurrentPost;
9979             if (this.ExistCurrentPost && post != null)
9980             {
9981                 if (post.IsDm || !this.StatusText.Enabled)
9982                     return;
9983
9984                 if (post.IsProtect)
9985                 {
9986                     MessageBox.Show("Protected.");
9987                     return;
9988                 }
9989                 var rtdata = post.Text;
9990                 rtdata = CreateRetweetUnofficial(rtdata, this.StatusText.Multiline);
9991
9992                 var selection = (this.StatusText.SelectionStart, this.StatusText.SelectionLength);
9993
9994                 // 投稿時に in_reply_to_status_id を付加する
9995                 var inReplyToStatusId = post.RetweetedId ?? post.StatusId;
9996                 var inReplyToScreenName = post.ScreenName;
9997                 this.inReplyTo = (inReplyToStatusId, inReplyToScreenName);
9998
9999                 this.StatusText.Text += " RT @" + post.ScreenName + ": " + rtdata;
10000
10001                 (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection;
10002                 this.StatusText.Focus();
10003             }
10004         }
10005
10006         private void QuoteStripMenuItem_Click(object sender, EventArgs e)
10007             => this.DoQuoteOfficial();
10008
10009         private async void SearchButton_Click(object sender, EventArgs e)
10010         {
10011             // 公式検索
10012             var pnl = ((Control)sender).Parent;
10013             if (pnl == null) return;
10014             var tbName = pnl.Parent.Text;
10015             var tb = (PublicSearchTabModel)this.statuses.Tabs[tbName];
10016             var cmb = (ComboBox)pnl.Controls["comboSearch"];
10017             var cmbLang = (ComboBox)pnl.Controls["comboLang"];
10018             cmb.Text = cmb.Text.Trim();
10019             // 検索式演算子 OR についてのみ大文字しか認識しないので強制的に大文字とする
10020             var quote = false;
10021             var buf = new StringBuilder();
10022             var c = cmb.Text.ToCharArray();
10023             for (var cnt = 0; cnt < cmb.Text.Length; cnt++)
10024             {
10025                 if (cnt > cmb.Text.Length - 4)
10026                 {
10027                     buf.Append(cmb.Text.Substring(cnt));
10028                     break;
10029                 }
10030                 if (c[cnt] == '"')
10031                 {
10032                     quote = !quote;
10033                 }
10034                 else
10035                 {
10036                     if (!quote && cmb.Text.Substring(cnt, 4).Equals(" or ", StringComparison.OrdinalIgnoreCase))
10037                     {
10038                         buf.Append(" OR ");
10039                         cnt += 3;
10040                         continue;
10041                     }
10042                 }
10043                 buf.Append(c[cnt]);
10044             }
10045             cmb.Text = buf.ToString();
10046
10047             var listView = (DetailsListView)pnl.Parent.Tag;
10048
10049             var queryChanged = tb.SearchWords != cmb.Text || tb.SearchLang != cmbLang.Text;
10050
10051             tb.SearchWords = cmb.Text;
10052             tb.SearchLang = cmbLang.Text;
10053             if (MyCommon.IsNullOrEmpty(cmb.Text))
10054             {
10055                 listView.Focus();
10056                 this.SaveConfigsTabs();
10057                 return;
10058             }
10059             if (queryChanged)
10060             {
10061                 var idx = cmb.Items.IndexOf(tb.SearchWords);
10062                 if (idx > -1) cmb.Items.RemoveAt(idx);
10063                 cmb.Items.Insert(0, tb.SearchWords);
10064                 cmb.Text = tb.SearchWords;
10065                 cmb.SelectAll();
10066                 this.PurgeListViewItemCache();
10067                 listView.VirtualListSize = 0;
10068                 this.statuses.ClearTabIds(tbName);
10069                 this.SaveConfigsTabs();   // 検索条件の保存
10070             }
10071
10072             listView.Focus();
10073             await this.RefreshTabAsync(tb);
10074         }
10075
10076         private async void RefreshMoreStripMenuItem_Click(object sender, EventArgs e)
10077             => await this.DoRefreshMore(); // もっと前を取得
10078
10079         /// <summary>
10080         /// 指定されたタブのListTabにおける位置を返します
10081         /// </summary>
10082         /// <remarks>
10083         /// 非表示のタブについて -1 が返ることを常に考慮して下さい
10084         /// </remarks>
10085         public int GetTabPageIndex(string tabName)
10086             => this.statuses.Tabs.IndexOf(tabName);
10087
10088         private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e)
10089         {
10090             if (this.statuses.RemovedTab.Count == 0)
10091             {
10092                 MessageBox.Show("There isn't removed tab.", "Undo", MessageBoxButtons.OK, MessageBoxIcon.Information);
10093                 return;
10094             }
10095             else
10096             {
10097                 DetailsListView? listView;
10098
10099                 var tb = this.statuses.RemovedTab.Pop();
10100                 if (tb.TabType == MyCommon.TabUsageType.Related)
10101                 {
10102                     var relatedTab = this.statuses.GetTabByType(MyCommon.TabUsageType.Related);
10103                     if (relatedTab != null)
10104                     {
10105                         // 関連発言なら既存のタブを置き換える
10106                         tb.TabName = relatedTab.TabName;
10107                         this.ClearTab(tb.TabName, false);
10108
10109                         this.statuses.ReplaceTab(tb);
10110
10111                         var tabIndex = this.statuses.Tabs.IndexOf(tb);
10112                         var tabPage = this.ListTab.TabPages[tabIndex];
10113                         listView = (DetailsListView)tabPage.Tag;
10114                         this.ListTab.SelectedIndex = tabIndex;
10115                     }
10116                     else
10117                     {
10118                         const string TabName = "Related Tweets";
10119                         var renamed = TabName;
10120                         for (var i = 2; i <= 100; i++)
10121                         {
10122                             if (!this.statuses.ContainsTab(renamed))
10123                                 break;
10124                             renamed = TabName + i;
10125                         }
10126                         tb.TabName = renamed;
10127
10128                         this.statuses.AddTab(tb);
10129                         this.AddNewTab(tb, startup: false);
10130
10131                         var tabIndex = this.statuses.Tabs.Count - 1;
10132                         var tabPage = this.ListTab.TabPages[tabIndex];
10133
10134                         listView = (DetailsListView)tabPage.Tag;
10135                         this.ListTab.SelectedIndex = tabIndex;
10136                     }
10137                 }
10138                 else
10139                 {
10140                     var renamed = tb.TabName;
10141                     for (var i = 1; i < int.MaxValue; i++)
10142                     {
10143                         if (!this.statuses.ContainsTab(renamed))
10144                             break;
10145                         renamed = tb.TabName + "(" + i + ")";
10146                     }
10147                     tb.TabName = renamed;
10148
10149                     this.statuses.AddTab(tb);
10150                     this.AddNewTab(tb, startup: false);
10151
10152                     var tabIndex = this.statuses.Tabs.Count - 1;
10153                     var tabPage = this.ListTab.TabPages[tabIndex];
10154
10155                     listView = (DetailsListView)tabPage.Tag;
10156                     this.ListTab.SelectedIndex = tabIndex;
10157                 }
10158                 this.SaveConfigsTabs();
10159
10160                 if (listView != null)
10161                 {
10162                     using (ControlTransaction.Update(listView))
10163                     {
10164                         listView.VirtualListSize = tb.AllCount;
10165                     }
10166                 }
10167             }
10168         }
10169
10170         private async Task DoMoveToRTHome()
10171         {
10172             var post = this.CurrentPost;
10173             if (post != null && post.RetweetedId != null)
10174                 await MyCommon.OpenInBrowserAsync(this, "https://twitter.com/" + post.RetweetedBy);
10175         }
10176
10177         private async void RetweetedByOpenInBrowserMenuItem_Click(object sender, EventArgs e)
10178             => await this.DoMoveToRTHome();
10179
10180         private void AuthorListManageMenuItem_Click(object sender, EventArgs e)
10181         {
10182             var screenName = this.CurrentPost?.ScreenName;
10183             if (screenName != null)
10184                 this.ListManageUserContext(screenName);
10185         }
10186
10187         private void RetweetedByListManageMenuItem_Click(object sender, EventArgs e)
10188         {
10189             var screenName = this.CurrentPost?.RetweetedBy;
10190             if (screenName != null)
10191                 this.ListManageUserContext(screenName);
10192         }
10193
10194         public void ListManageUserContext(string screenName)
10195         {
10196             using var listSelectForm = new MyLists(screenName, this.twitterApi);
10197             listSelectForm.ShowDialog(this);
10198         }
10199
10200         private void SearchControls_Enter(object sender, EventArgs e)
10201         {
10202             var pnl = (Control)sender;
10203             foreach (Control ctl in pnl.Controls)
10204             {
10205                 ctl.TabStop = true;
10206             }
10207         }
10208
10209         private void SearchControls_Leave(object sender, EventArgs e)
10210         {
10211             var pnl = (Control)sender;
10212             foreach (Control ctl in pnl.Controls)
10213             {
10214                 ctl.TabStop = false;
10215             }
10216         }
10217
10218         private void PublicSearchQueryMenuItem_Click(object sender, EventArgs e)
10219         {
10220             var tab = this.CurrentTab;
10221             if (tab.TabType != MyCommon.TabUsageType.PublicSearch) return;
10222             this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus();
10223         }
10224
10225         private void StatusLabel_DoubleClick(object sender, EventArgs e)
10226             => MessageBox.Show(this.StatusLabel.TextHistory, "Logs", MessageBoxButtons.OK, MessageBoxIcon.None);
10227
10228         private void HashManageMenuItem_Click(object sender, EventArgs e)
10229         {
10230             DialogResult rslt;
10231             try
10232             {
10233                 rslt = this.HashMgr.ShowDialog();
10234             }
10235             catch (Exception)
10236             {
10237                 return;
10238             }
10239             this.TopMost = SettingManager.Common.AlwaysTop;
10240             if (rslt == DialogResult.Cancel) return;
10241             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
10242             {
10243                 this.HashStripSplitButton.Text = this.HashMgr.UseHash;
10244                 this.HashTogglePullDownMenuItem.Checked = true;
10245                 this.HashToggleMenuItem.Checked = true;
10246             }
10247             else
10248             {
10249                 this.HashStripSplitButton.Text = "#[-]";
10250                 this.HashTogglePullDownMenuItem.Checked = false;
10251                 this.HashToggleMenuItem.Checked = false;
10252             }
10253
10254             this.MarkSettingCommonModified();
10255             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
10256         }
10257
10258         private void HashToggleMenuItem_Click(object sender, EventArgs e)
10259         {
10260             this.HashMgr.ToggleHash();
10261             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
10262             {
10263                 this.HashStripSplitButton.Text = this.HashMgr.UseHash;
10264                 this.HashToggleMenuItem.Checked = true;
10265                 this.HashTogglePullDownMenuItem.Checked = true;
10266             }
10267             else
10268             {
10269                 this.HashStripSplitButton.Text = "#[-]";
10270                 this.HashToggleMenuItem.Checked = false;
10271                 this.HashTogglePullDownMenuItem.Checked = false;
10272             }
10273             this.MarkSettingCommonModified();
10274             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
10275         }
10276
10277         private void HashStripSplitButton_ButtonClick(object sender, EventArgs e)
10278             => this.HashToggleMenuItem_Click(this.HashToggleMenuItem, EventArgs.Empty);
10279
10280         public void SetPermanentHashtag(string hashtag)
10281         {
10282             this.HashMgr.SetPermanentHash("#" + hashtag);
10283             this.HashStripSplitButton.Text = this.HashMgr.UseHash;
10284             this.HashTogglePullDownMenuItem.Checked = true;
10285             this.HashToggleMenuItem.Checked = true;
10286             // 使用ハッシュタグとして設定
10287             this.MarkSettingCommonModified();
10288         }
10289
10290         private void MenuItemOperate_DropDownOpening(object sender, EventArgs e)
10291         {
10292             var tab = this.CurrentTab;
10293             var post = this.CurrentPost;
10294             if (!this.ExistCurrentPost)
10295             {
10296                 this.ReplyOpMenuItem.Enabled = false;
10297                 this.ReplyAllOpMenuItem.Enabled = false;
10298                 this.DmOpMenuItem.Enabled = false;
10299                 this.CreateTabRuleOpMenuItem.Enabled = false;
10300                 this.CreateIdRuleOpMenuItem.Enabled = false;
10301                 this.CreateSourceRuleOpMenuItem.Enabled = false;
10302                 this.ReadOpMenuItem.Enabled = false;
10303                 this.UnreadOpMenuItem.Enabled = false;
10304                 this.AuthorMenuItem.Visible = false;
10305                 this.RetweetedByMenuItem.Visible = false;
10306             }
10307             else
10308             {
10309                 this.ReplyOpMenuItem.Enabled = true;
10310                 this.ReplyAllOpMenuItem.Enabled = true;
10311                 this.DmOpMenuItem.Enabled = true;
10312                 this.CreateTabRuleOpMenuItem.Enabled = true;
10313                 this.CreateIdRuleOpMenuItem.Enabled = true;
10314                 this.CreateSourceRuleOpMenuItem.Enabled = true;
10315                 this.ReadOpMenuItem.Enabled = true;
10316                 this.UnreadOpMenuItem.Enabled = true;
10317                 this.AuthorMenuItem.Visible = true;
10318                 this.AuthorMenuItem.Text = $"@{post!.ScreenName}";
10319                 this.RetweetedByMenuItem.Visible = post.RetweetedByUserId != null;
10320                 this.RetweetedByMenuItem.Text = $"@{post.RetweetedBy}";
10321             }
10322
10323             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm)
10324             {
10325                 this.FavOpMenuItem.Enabled = false;
10326                 this.UnFavOpMenuItem.Enabled = false;
10327                 this.OpenStatusOpMenuItem.Enabled = false;
10328                 this.ShowRelatedStatusesMenuItem2.Enabled = false;
10329                 this.RtOpMenuItem.Enabled = false;
10330                 this.RtUnOpMenuItem.Enabled = false;
10331                 this.QtOpMenuItem.Enabled = false;
10332                 this.FavoriteRetweetMenuItem.Enabled = false;
10333                 this.FavoriteRetweetUnofficialMenuItem.Enabled = false;
10334             }
10335             else
10336             {
10337                 this.FavOpMenuItem.Enabled = true;
10338                 this.UnFavOpMenuItem.Enabled = true;
10339                 this.OpenStatusOpMenuItem.Enabled = true;
10340                 this.ShowRelatedStatusesMenuItem2.Enabled = true;  // PublicSearchの時問題出るかも
10341
10342                 if (!post.CanRetweetBy(this.twitterApi.CurrentUserId))
10343                 {
10344                     this.RtOpMenuItem.Enabled = false;
10345                     this.RtUnOpMenuItem.Enabled = false;
10346                     this.QtOpMenuItem.Enabled = false;
10347                     this.FavoriteRetweetMenuItem.Enabled = false;
10348                     this.FavoriteRetweetUnofficialMenuItem.Enabled = false;
10349                 }
10350                 else
10351                 {
10352                     this.RtOpMenuItem.Enabled = true;
10353                     this.RtUnOpMenuItem.Enabled = true;
10354                     this.QtOpMenuItem.Enabled = true;
10355                     this.FavoriteRetweetMenuItem.Enabled = true;
10356                     this.FavoriteRetweetUnofficialMenuItem.Enabled = true;
10357                 }
10358             }
10359
10360             if (tab.TabType != MyCommon.TabUsageType.Favorites)
10361             {
10362                 this.RefreshPrevOpMenuItem.Enabled = true;
10363             }
10364             else
10365             {
10366                 this.RefreshPrevOpMenuItem.Enabled = false;
10367             }
10368             if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null)
10369             {
10370                 this.OpenRepSourceOpMenuItem.Enabled = false;
10371             }
10372             else
10373             {
10374                 this.OpenRepSourceOpMenuItem.Enabled = true;
10375             }
10376
10377             if (this.ExistCurrentPost && post != null)
10378             {
10379                 this.DelOpMenuItem.Enabled = post.CanDeleteBy(this.tw.UserId);
10380             }
10381         }
10382
10383         private void MenuItemTab_DropDownOpening(object sender, EventArgs e)
10384             => this.ContextMenuTabProperty_Opening(sender, null!);
10385
10386         public Twitter TwitterInstance
10387             => this.tw;
10388
10389         private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e)
10390         {
10391             if (this.initialLayout)
10392                 return;
10393
10394             int splitterDistance;
10395             switch (this.WindowState)
10396             {
10397                 case FormWindowState.Normal:
10398                     splitterDistance = this.SplitContainer3.SplitterDistance;
10399                     break;
10400                 case FormWindowState.Maximized:
10401                     // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する
10402                     var normalContainerWidth = this.mySize.Width - SystemInformation.Border3DSize.Width * 2;
10403                     splitterDistance = this.SplitContainer3.SplitterDistance - (this.SplitContainer3.Width - normalContainerWidth);
10404                     splitterDistance = Math.Min(splitterDistance, normalContainerWidth - this.SplitContainer3.SplitterWidth - this.SplitContainer3.Panel2MinSize);
10405                     break;
10406                 default:
10407                     return;
10408             }
10409
10410             this.mySpDis3 = splitterDistance;
10411             this.MarkSettingLocalModified();
10412         }
10413
10414         private void MenuItemEdit_DropDownOpening(object sender, EventArgs e)
10415         {
10416             if (this.statuses.RemovedTab.Count == 0)
10417             {
10418                 this.UndoRemoveTabMenuItem.Enabled = false;
10419             }
10420             else
10421             {
10422                 this.UndoRemoveTabMenuItem.Enabled = true;
10423             }
10424
10425             if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
10426                 this.PublicSearchQueryMenuItem.Enabled = true;
10427             else
10428                 this.PublicSearchQueryMenuItem.Enabled = false;
10429
10430             var post = this.CurrentPost;
10431             if (!this.ExistCurrentPost || post == null)
10432             {
10433                 this.CopySTOTMenuItem.Enabled = false;
10434                 this.CopyURLMenuItem.Enabled = false;
10435                 this.CopyUserIdStripMenuItem.Enabled = false;
10436             }
10437             else
10438             {
10439                 this.CopySTOTMenuItem.Enabled = true;
10440                 this.CopyURLMenuItem.Enabled = true;
10441                 this.CopyUserIdStripMenuItem.Enabled = true;
10442
10443                 if (post.IsDm) this.CopyURLMenuItem.Enabled = false;
10444                 if (post.IsProtect) this.CopySTOTMenuItem.Enabled = false;
10445             }
10446         }
10447
10448         private void NotifyIcon1_MouseMove(object sender, MouseEventArgs e)
10449             => this.SetNotifyIconText();
10450
10451         private async void UserStatusToolStripMenuItem_Click(object sender, EventArgs e)
10452             => await this.ShowUserStatus(this.CurrentPost?.ScreenName ?? "");
10453
10454         private async Task DoShowUserStatus(string id, bool showInputDialog)
10455         {
10456             TwitterUser? user = null;
10457
10458             if (showInputDialog)
10459             {
10460                 using var inputName = new InputTabName();
10461                 inputName.FormTitle = "Show UserStatus";
10462                 inputName.FormDescription = Properties.Resources.FRMessage1;
10463                 inputName.TabName = id;
10464
10465                 if (inputName.ShowDialog(this) != DialogResult.OK)
10466                     return;
10467                 if (string.IsNullOrWhiteSpace(inputName.TabName))
10468                     return;
10469
10470                 id = inputName.TabName.Trim();
10471             }
10472
10473             using (var dialog = new WaitingDialog(Properties.Resources.doShowUserStatusText1))
10474             {
10475                 var cancellationToken = dialog.EnableCancellation();
10476
10477                 try
10478                 {
10479                     var task = this.twitterApi.UsersShow(id);
10480                     user = await dialog.WaitForAsync(this, task);
10481                 }
10482                 catch (WebApiException ex)
10483                 {
10484                     if (!cancellationToken.IsCancellationRequested)
10485                         MessageBox.Show($"Err:{ex.Message}(UsersShow)");
10486                     return;
10487                 }
10488
10489                 if (cancellationToken.IsCancellationRequested)
10490                     return;
10491             }
10492
10493             await this.DoShowUserStatus(user);
10494         }
10495
10496         private async Task DoShowUserStatus(TwitterUser user)
10497         {
10498             using var userDialog = new UserInfoDialog(this, this.twitterApi);
10499             var showUserTask = userDialog.ShowUserAsync(user);
10500             userDialog.ShowDialog(this);
10501
10502             this.Activate();
10503             this.BringToFront();
10504
10505             // ユーザー情報の表示が完了するまで userDialog を破棄しない
10506             await showUserTask;
10507         }
10508
10509         internal Task ShowUserStatus(string id, bool showInputDialog)
10510             => this.DoShowUserStatus(id, showInputDialog);
10511
10512         internal Task ShowUserStatus(string id)
10513             => this.DoShowUserStatus(id, true);
10514
10515         private async void AuthorShowProfileMenuItem_Click(object sender, EventArgs e)
10516         {
10517             var post = this.CurrentPost;
10518             if (post != null)
10519             {
10520                 await this.ShowUserStatus(post.ScreenName, false);
10521             }
10522         }
10523
10524         private async void RetweetedByShowProfileMenuItem_Click(object sender, EventArgs e)
10525         {
10526             var retweetedBy = this.CurrentPost?.RetweetedBy;
10527             if (retweetedBy != null)
10528             {
10529                 await this.ShowUserStatus(retweetedBy, false);
10530             }
10531         }
10532
10533         private async void RtCountMenuItem_Click(object sender, EventArgs e)
10534         {
10535             var post = this.CurrentPost;
10536             if (!this.ExistCurrentPost || post == null)
10537                 return;
10538
10539             var statusId = post.RetweetedId ?? post.StatusId;
10540             TwitterStatus status;
10541
10542             using (var dialog = new WaitingDialog(Properties.Resources.RtCountMenuItem_ClickText1))
10543             {
10544                 var cancellationToken = dialog.EnableCancellation();
10545
10546                 try
10547                 {
10548                     var task = this.twitterApi.StatusesShow(statusId);
10549                     status = await dialog.WaitForAsync(this, task);
10550                 }
10551                 catch (WebApiException ex)
10552                 {
10553                     if (!cancellationToken.IsCancellationRequested)
10554                         MessageBox.Show(Properties.Resources.RtCountText2 + Environment.NewLine + "Err:" + ex.Message);
10555                     return;
10556                 }
10557
10558                 if (cancellationToken.IsCancellationRequested)
10559                     return;
10560             }
10561
10562             MessageBox.Show(status.RetweetCount + Properties.Resources.RtCountText1);
10563         }
10564
10565         private readonly HookGlobalHotkey hookGlobalHotkey;
10566
10567         public TweenMain()
10568         {
10569             this.hookGlobalHotkey = new HookGlobalHotkey(this);
10570
10571             // この呼び出しは、Windows フォーム デザイナで必要です。
10572             this.InitializeComponent();
10573
10574             // InitializeComponent() 呼び出しの後で初期化を追加します。
10575
10576             if (!this.DesignMode)
10577             {
10578                 // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策
10579                 this.StatusText.Dock = DockStyle.Fill;
10580             }
10581
10582             this.tweetDetailsView.Owner = this;
10583
10584             this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed;
10585             this.gh.NotifyClicked += this.GrowlHelper_Callback;
10586
10587             // メイリオフォント指定時にタブの最小幅が広くなる問題の対策
10588             this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40);
10589
10590             this.ImageSelector.Visible = false;
10591             this.ImageSelector.Enabled = false;
10592             this.ImageSelector.FilePickDialog = this.OpenFileDialog1;
10593
10594             this.workerProgress = new Progress<string>(x => this.StatusLabel.Text = x);
10595
10596             this.ReplaceAppName();
10597             this.InitializeShortcuts();
10598         }
10599
10600         private void HookGlobalHotkey_HotkeyPressed(object sender, KeyEventArgs e)
10601         {
10602             if ((this.WindowState == FormWindowState.Normal || this.WindowState == FormWindowState.Maximized) && this.Visible && Form.ActiveForm == this)
10603             {
10604                 // アイコン化
10605                 this.Visible = false;
10606             }
10607             else if (Form.ActiveForm == null)
10608             {
10609                 this.Visible = true;
10610                 if (this.WindowState == FormWindowState.Minimized) this.WindowState = FormWindowState.Normal;
10611                 this.Activate();
10612                 this.BringToFront();
10613                 this.StatusText.Focus();
10614             }
10615         }
10616
10617         private void SplitContainer2_MouseDoubleClick(object sender, MouseEventArgs e)
10618             => this.MultiLinePullDownMenuItem.PerformClick();
10619
10620 #region "画像投稿"
10621         private void ImageSelectMenuItem_Click(object sender, EventArgs e)
10622         {
10623             if (this.ImageSelector.Visible)
10624                 this.ImageSelector.EndSelection();
10625             else
10626                 this.ImageSelector.BeginSelection();
10627         }
10628
10629         private void SelectMedia_DragEnter(DragEventArgs e)
10630         {
10631             if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
10632             {
10633                 e.Effect = DragDropEffects.Copy;
10634                 return;
10635             }
10636             e.Effect = DragDropEffects.None;
10637         }
10638
10639         private void SelectMedia_DragDrop(DragEventArgs e)
10640         {
10641             this.Activate();
10642             this.BringToFront();
10643             this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false));
10644             this.StatusText.Focus();
10645         }
10646
10647         private void ImageSelector_BeginSelecting(object sender, EventArgs e)
10648         {
10649             this.TimelinePanel.Visible = false;
10650             this.TimelinePanel.Enabled = false;
10651         }
10652
10653         private void ImageSelector_EndSelecting(object sender, EventArgs e)
10654         {
10655             this.TimelinePanel.Visible = true;
10656             this.TimelinePanel.Enabled = true;
10657             this.CurrentListView.Focus();
10658         }
10659
10660         private void ImageSelector_FilePickDialogOpening(object sender, EventArgs e)
10661             => this.AllowDrop = false;
10662
10663         private void ImageSelector_FilePickDialogClosed(object sender, EventArgs e)
10664             => this.AllowDrop = true;
10665
10666         private void ImageSelector_SelectedServiceChanged(object sender, EventArgs e)
10667         {
10668             if (this.ImageSelector.Visible)
10669             {
10670                 this.MarkSettingCommonModified();
10671                 this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
10672             }
10673         }
10674
10675         private void ImageSelector_VisibleChanged(object sender, EventArgs e)
10676             => this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
10677
10678         /// <summary>
10679         /// StatusTextでCtrl+Vが押下された時の処理
10680         /// </summary>
10681         private void ProcClipboardFromStatusTextWhenCtrlPlusV()
10682         {
10683             try
10684             {
10685                 if (Clipboard.ContainsText())
10686                 {
10687                     // clipboardにテキストがある場合は貼り付け処理
10688                     this.StatusText.Paste(Clipboard.GetText());
10689                 }
10690                 else if (Clipboard.ContainsImage())
10691                 {
10692                     // 画像があるので投稿処理を行う
10693                     if (MessageBox.Show(Properties.Resources.PostPictureConfirm3,
10694                                        Properties.Resources.PostPictureWarn4,
10695                                        MessageBoxButtons.OKCancel,
10696                                        MessageBoxIcon.Question,
10697                                        MessageBoxDefaultButton.Button2)
10698                                    == DialogResult.OK)
10699                     {
10700                         // clipboardから画像を取得
10701                         using var image = Clipboard.GetImage();
10702                         this.ImageSelector.BeginSelection(image);
10703                     }
10704                 }
10705                 else if (Clipboard.ContainsFileDropList())
10706                 {
10707                     var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
10708                     this.ImageSelector.BeginSelection(files);
10709                 }
10710             }
10711             catch (ExternalException ex)
10712             {
10713                 MessageBox.Show(ex.Message);
10714             }
10715         }
10716 #endregion
10717
10718         private void ListManageToolStripMenuItem_Click(object sender, EventArgs e)
10719         {
10720             using var form = new ListManage(this.tw);
10721             form.ShowDialog(this);
10722         }
10723
10724         private bool ModifySettingCommon { get; set; }
10725
10726         private bool ModifySettingLocal { get; set; }
10727
10728         private bool ModifySettingAtId { get; set; }
10729
10730         private void MenuItemCommand_DropDownOpening(object sender, EventArgs e)
10731         {
10732             var post = this.CurrentPost;
10733             if (this.ExistCurrentPost && post != null && !post.IsDm)
10734                 this.RtCountMenuItem.Enabled = true;
10735             else
10736                 this.RtCountMenuItem.Enabled = false;
10737         }
10738
10739         private void CopyUserIdStripMenuItem_Click(object sender, EventArgs e)
10740             => this.CopyUserId();
10741
10742         private void CopyUserId()
10743         {
10744             var post = this.CurrentPost;
10745             if (post == null) return;
10746             var clstr = post.ScreenName;
10747             try
10748             {
10749                 Clipboard.SetDataObject(clstr, false, 5, 100);
10750             }
10751             catch (Exception ex)
10752             {
10753                 MessageBox.Show(ex.Message);
10754             }
10755         }
10756
10757         private async void ShowRelatedStatusesMenuItem_Click(object sender, EventArgs e)
10758         {
10759             var post = this.CurrentPost;
10760             if (this.ExistCurrentPost && post != null && !post.IsDm)
10761             {
10762                 try
10763                 {
10764                     await this.OpenRelatedTab(post);
10765                 }
10766                 catch (TabException ex)
10767                 {
10768                     MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
10769                 }
10770             }
10771         }
10772
10773         /// <summary>
10774         /// 指定されたツイートに対する関連発言タブを開きます
10775         /// </summary>
10776         /// <param name="statusId">表示するツイートのID</param>
10777         /// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
10778         public async Task OpenRelatedTab(long statusId)
10779         {
10780             var post = this.statuses[statusId];
10781             if (post == null)
10782             {
10783                 try
10784                 {
10785                     post = await this.tw.GetStatusApi(false, statusId);
10786                 }
10787                 catch (WebApiException ex)
10788                 {
10789                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
10790                     return;
10791                 }
10792             }
10793
10794             await this.OpenRelatedTab(post);
10795         }
10796
10797         /// <summary>
10798         /// 指定されたツイートに対する関連発言タブを開きます
10799         /// </summary>
10800         /// <param name="post">表示する対象となるツイート</param>
10801         /// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
10802         private async Task OpenRelatedTab(PostClass post)
10803         {
10804             var tabRelated = this.statuses.GetTabByType<RelatedPostsTabModel>();
10805             if (tabRelated != null)
10806             {
10807                 this.RemoveSpecifiedTab(tabRelated.TabName, confirm: false);
10808             }
10809
10810             var tabName = this.statuses.MakeTabName("Related Tweets");
10811
10812             tabRelated = new RelatedPostsTabModel(tabName, post)
10813             {
10814                 UnreadManage = false,
10815                 Notify = false,
10816             };
10817
10818             this.statuses.AddTab(tabRelated);
10819             this.AddNewTab(tabRelated, startup: false);
10820
10821             this.ListTab.SelectedIndex = this.statuses.Tabs.IndexOf(tabName);
10822
10823             await this.RefreshTabAsync(tabRelated);
10824
10825             var tabIndex = this.statuses.Tabs.IndexOf(tabRelated.TabName);
10826
10827             if (tabIndex != -1)
10828             {
10829                 // TODO: 非同期更新中にタブが閉じられている場合を厳密に考慮したい
10830
10831                 var tabPage = this.ListTab.TabPages[tabIndex];
10832                 var listView = (DetailsListView)tabPage.Tag;
10833                 var targetPost = tabRelated.TargetPost;
10834                 var index = tabRelated.IndexOf(targetPost.RetweetedId ?? targetPost.StatusId);
10835
10836                 if (index != -1 && index < listView.Items.Count)
10837                 {
10838                     listView.SelectedIndices.Add(index);
10839                     listView.Items[index].Focused = true;
10840                 }
10841             }
10842         }
10843
10844         private void CacheInfoMenuItem_Click(object sender, EventArgs e)
10845         {
10846             var buf = new StringBuilder();
10847             buf.AppendFormat("キャッシュエントリ保持数     : {0}" + Environment.NewLine, this.iconCache.CacheCount);
10848             buf.AppendFormat("キャッシュエントリ破棄数     : {0}" + Environment.NewLine, this.iconCache.CacheRemoveCount);
10849             MessageBox.Show(buf.ToString(), "アイコンキャッシュ使用状況");
10850         }
10851
10852         private void TweenRestartMenuItem_Click(object sender, EventArgs e)
10853         {
10854             MyCommon.EndingFlag = true;
10855             try
10856             {
10857                 this.Close();
10858                 Application.Restart();
10859             }
10860             catch (Exception)
10861             {
10862                 MessageBox.Show("Failed to restart. Please run " + ApplicationSettings.ApplicationName + " manually.");
10863             }
10864         }
10865
10866         private async void OpenOwnHomeMenuItem_Click(object sender, EventArgs e)
10867             => await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + this.tw.Username);
10868
10869         private bool ExistCurrentPost
10870         {
10871             get
10872             {
10873                 var post = this.CurrentPost;
10874                 return post != null && !post.IsDeleted;
10875             }
10876         }
10877
10878         private async void AuthorShowUserTimelineMenuItem_Click(object sender, EventArgs e)
10879             => await this.ShowUserTimeline();
10880
10881         private async void RetweetedByShowUserTimelineMenuItem_Click(object sender, EventArgs e)
10882             => await this.ShowRetweeterTimeline();
10883
10884         private string GetUserIdFromCurPostOrInput(string caption)
10885         {
10886             var id = this.CurrentPost?.ScreenName ?? "";
10887
10888             using var inputName = new InputTabName();
10889             inputName.FormTitle = caption;
10890             inputName.FormDescription = Properties.Resources.FRMessage1;
10891             inputName.TabName = id;
10892
10893             if (inputName.ShowDialog() == DialogResult.OK &&
10894                 !MyCommon.IsNullOrEmpty(inputName.TabName.Trim()))
10895             {
10896                 id = inputName.TabName.Trim();
10897             }
10898             else
10899             {
10900                 id = "";
10901             }
10902             return id;
10903         }
10904
10905         private async void UserTimelineToolStripMenuItem_Click(object sender, EventArgs e)
10906         {
10907             var id = this.GetUserIdFromCurPostOrInput("Show UserTimeline");
10908             if (!MyCommon.IsNullOrEmpty(id))
10909             {
10910                 await this.AddNewTabForUserTimeline(id);
10911             }
10912         }
10913
10914         private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e)
10915         {
10916             if (e.Mode == Microsoft.Win32.PowerModes.Resume)
10917                 this.timelineScheduler.SystemResumed();
10918         }
10919
10920         private void SystemEvents_TimeChanged(object sender, EventArgs e)
10921         {
10922             var prevTimeOffset = TimeZoneInfo.Local.BaseUtcOffset;
10923
10924             TimeZoneInfo.ClearCachedData();
10925
10926             var curTimeOffset = TimeZoneInfo.Local.BaseUtcOffset;
10927
10928             if (curTimeOffset != prevTimeOffset)
10929             {
10930                 // タイムゾーンの変更を反映
10931                 this.PurgeListViewItemCache();
10932                 this.CurrentListView.Refresh();
10933
10934                 this.DispSelectedPost(forceupdate: true);
10935             }
10936
10937             this.timelineScheduler.Reset();
10938         }
10939
10940         private void TimelineRefreshEnableChange(bool isEnable)
10941         {
10942             this.timelineScheduler.Enabled = isEnable;
10943         }
10944
10945         private void StopRefreshAllMenuItem_CheckedChanged(object sender, EventArgs e)
10946             => this.TimelineRefreshEnableChange(!this.StopRefreshAllMenuItem.Checked);
10947
10948         private async Task OpenUserAppointUrl()
10949         {
10950             if (SettingManager.Common.UserAppointUrl != null)
10951             {
10952                 if (SettingManager.Common.UserAppointUrl.Contains("{ID}") || SettingManager.Common.UserAppointUrl.Contains("{STATUS}"))
10953                 {
10954                     var post = this.CurrentPost;
10955                     if (post != null)
10956                     {
10957                         var xUrl = SettingManager.Common.UserAppointUrl;
10958                         xUrl = xUrl.Replace("{ID}", post.ScreenName);
10959
10960                         var statusId = post.RetweetedId ?? post.StatusId;
10961                         xUrl = xUrl.Replace("{STATUS}", statusId.ToString());
10962
10963                         await MyCommon.OpenInBrowserAsync(this, xUrl);
10964                     }
10965                 }
10966                 else
10967                 {
10968                     await MyCommon.OpenInBrowserAsync(this, SettingManager.Common.UserAppointUrl);
10969                 }
10970             }
10971         }
10972
10973         private async void OpenUserSpecifiedUrlMenuItem_Click(object sender, EventArgs e)
10974             => await this.OpenUserAppointUrl();
10975
10976         private async void GrowlHelper_Callback(object sender, GrowlHelper.NotifyCallbackEventArgs e)
10977         {
10978             if (Form.ActiveForm == null)
10979             {
10980                 await this.InvokeAsync(() =>
10981                 {
10982                     this.Visible = true;
10983                     if (this.WindowState == FormWindowState.Minimized) this.WindowState = FormWindowState.Normal;
10984                     this.Activate();
10985                     this.BringToFront();
10986                     if (e.NotifyType == GrowlHelper.NotifyType.DirectMessage)
10987                     {
10988                         if (!this.GoDirectMessage(e.StatusId)) this.StatusText.Focus();
10989                     }
10990                     else
10991                     {
10992                         if (!this.GoStatus(e.StatusId)) this.StatusText.Focus();
10993                     }
10994                 });
10995             }
10996         }
10997
10998         private void ReplaceAppName()
10999         {
11000             this.MatomeMenuItem.Text = MyCommon.ReplaceAppName(this.MatomeMenuItem.Text);
11001             this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text);
11002         }
11003
11004         private void TweetThumbnail_ThumbnailLoading(object sender, EventArgs e)
11005             => this.SplitContainer3.Panel2Collapsed = false;
11006
11007         private async void TweetThumbnail_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e)
11008             => await this.OpenThumbnailPicture(e.Thumbnail);
11009
11010         private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e)
11011             => await MyCommon.OpenInBrowserAsync(this, e.ImageUrl);
11012
11013         private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail)
11014         {
11015             var url = thumbnail.FullSizeImageUrl ?? thumbnail.MediaPageUrl;
11016
11017             await MyCommon.OpenInBrowserAsync(this, url);
11018         }
11019
11020         private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
11021             => await MyCommon.OpenInBrowserAsync(this, Twitter.ServiceAvailabilityStatusUrl);
11022
11023         private void PostButton_KeyDown(object sender, KeyEventArgs e)
11024         {
11025             if (e.KeyCode == Keys.Space)
11026             {
11027                 this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
11028
11029                 e.SuppressKeyPress = true;
11030             }
11031         }
11032
11033         private void ContextMenuColumnHeader_Opening(object sender, CancelEventArgs e)
11034         {
11035             this.IconSizeNoneToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.IconNone;
11036             this.IconSize16ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon16;
11037             this.IconSize24ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon24;
11038             this.IconSize48ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon48;
11039             this.IconSize48_2ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon48_2;
11040
11041             this.LockListSortOrderToolStripMenuItem.Checked = SettingManager.Common.SortOrderLock;
11042         }
11043
11044         private void IconSizeNoneToolStripMenuItem_Click(object sender, EventArgs e)
11045             => this.ChangeListViewIconSize(MyCommon.IconSizes.IconNone);
11046
11047         private void IconSize16ToolStripMenuItem_Click(object sender, EventArgs e)
11048             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon16);
11049
11050         private void IconSize24ToolStripMenuItem_Click(object sender, EventArgs e)
11051             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon24);
11052
11053         private void IconSize48ToolStripMenuItem_Click(object sender, EventArgs e)
11054             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon48);
11055
11056         private void IconSize48_2ToolStripMenuItem_Click(object sender, EventArgs e)
11057             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon48_2);
11058
11059         private void ChangeListViewIconSize(MyCommon.IconSizes iconSize)
11060         {
11061             if (SettingManager.Common.IconSize == iconSize) return;
11062
11063             var oldIconCol = this.iconCol;
11064
11065             SettingManager.Common.IconSize = iconSize;
11066             this.ApplyListViewIconSize(iconSize);
11067
11068             if (this.iconCol != oldIconCol)
11069             {
11070                 foreach (TabPage tp in this.ListTab.TabPages)
11071                 {
11072                     this.ResetColumns((DetailsListView)tp.Tag);
11073                 }
11074             }
11075
11076             this.CurrentListView.Refresh();
11077             this.MarkSettingCommonModified();
11078         }
11079
11080         private void LockListSortToolStripMenuItem_Click(object sender, EventArgs e)
11081         {
11082             var state = this.LockListSortOrderToolStripMenuItem.Checked;
11083             if (SettingManager.Common.SortOrderLock == state) return;
11084
11085             SettingManager.Common.SortOrderLock = state;
11086             this.MarkSettingCommonModified();
11087         }
11088
11089         private void TweetDetailsView_StatusChanged(object sender, TweetDetailsViewStatusChengedEventArgs e)
11090         {
11091             if (!MyCommon.IsNullOrEmpty(e.StatusText))
11092             {
11093                 this.StatusLabelUrl.Text = e.StatusText;
11094             }
11095             else
11096             {
11097                 this.SetStatusLabelUrl();
11098             }
11099         }
11100     }
11101 }