OSDN Git Service

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