OSDN Git Service

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