OSDN Git Service

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