OSDN Git Service

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