1using System; 2using System.Drawing; 3using System.Runtime.InteropServices; 4using System.Windows.Forms; 5using WindowCapture.Models; 6using WindowCapture.UI; 7 8namespace WindowCapture.App 9{ 10 public class TrayApp : ApplicationContext 11 { 12 private static TrayApp instance; 13 public static TrayApp Instance { get { return instance; } } 14 15 private NotifyIcon trayIcon; 16 private Controller controller; 17 private ContextMenuStrip contextMenu; 18 19 // Hidden form for UI thread marshaling 20 private Form hiddenForm; 21 22 public TrayApp() : this(AppMode.Screenshot) { } 23 24 public TrayApp(AppMode mode) 25 { 26 instance = this; 27 28 // Create hidden form for BeginInvoke support 29 hiddenForm = new Form(); 30 hiddenForm.ShowInTaskbar = false; 31 hiddenForm.FormBorderStyle = FormBorderStyle.None; 32 hiddenForm.Size = new Size(1, 1); 33 hiddenForm.StartPosition = FormStartPosition.Manual; 34 hiddenForm.Location = new Point(-1000, -1000); 35 hiddenForm.Show(); 36 hiddenForm.Hide(); 37 38 contextMenu = new ContextMenuStrip(); 39 ControllerMode cmode; 40 41 if (mode == AppMode.MediaCore) 42 { 43 // Unified app: install every hook up front (All), then gate each module live via the 44 // tray toggles below (controller.SetModuleEnabled). One process, one tray icon. 45 cmode = ControllerMode.All; 46 if (Settings.ModuleClipboard) ClipboardForm.InstallListener(); 47 if (Settings.ModuleTextAssist) 48 { 49 WindowCapture.Helpers.SageClient.WarmUp(); 50 if (Settings.ContextNnRescore) WindowCapture.Helpers.RescoreClient.WarmUp(); 51 if (Settings.TsfBridge) WindowCapture.Helpers.TipBridge.Start(); 52 } 53 54 var miModShot = new ToolStripMenuItem("Скриншот (" + Settings.ActivationKeyName + "+Win)") { Checked = Settings.ModuleScreenshot, CheckOnClick = true }; 55 miModShot.Click += delegate { Settings.ModuleScreenshot = miModShot.Checked; Settings.Save(); ApplyModule(ControllerMode.Capture, Settings.ModuleScreenshot); }; 56 57 var miModText = new ToolStripMenuItem("Автозамена") { Checked = Settings.ModuleTextAssist, CheckOnClick = true }; 58 miModText.Click += delegate { 59 Settings.ModuleTextAssist = miModText.Checked; Settings.Save(); 60 ApplyModule(ControllerMode.TextAssist, Settings.ModuleTextAssist); 61 if (Settings.ModuleTextAssist) { WindowCapture.Helpers.SageClient.WarmUp(); if (Settings.TsfBridge) WindowCapture.Helpers.TipBridge.Start(); } 62 }; 63 AddAutocorrectToggles(miModText.DropDownItems); // sub-options live under "Автозамена" 64 65 var miModClip = new ToolStripMenuItem("Буфер обмена") { Checked = Settings.ModuleClipboard, CheckOnClick = true }; 66 miModClip.Click += delegate { 67 Settings.ModuleClipboard = miModClip.Checked; Settings.Save(); 68 ApplyModule(ControllerMode.Clipboard, Settings.ModuleClipboard); 69 if (Settings.ModuleClipboard) ClipboardForm.InstallListener(); 70 }; 71 var miClipHist = new ToolStripMenuItem("История буфера (Ctrl+V удержание)") { Checked = Settings.ClipboardHistory, CheckOnClick = true }; 72 miClipHist.Click += delegate { Settings.ClipboardHistory = miClipHist.Checked; Settings.Save(); }; 73 miModClip.DropDownItems.Add(miClipHist); 74 75 var miModGest = new ToolStripMenuItem("Жесты мышью (СКМ-перетаскивание)") { Checked = Settings.ModuleGestures, CheckOnClick = true }; 76 miModGest.Click += delegate { Settings.ModuleGestures = miModGest.Checked; Settings.Save(); ApplyModule(ControllerMode.Gestures, Settings.ModuleGestures); }; 77 78 contextMenu.Items.Add(miModShot); 79 contextMenu.Items.Add(miModText); 80 contextMenu.Items.Add(miModClip); 81 contextMenu.Items.Add(miModGest); 82 contextMenu.Items.Add("-"); 83 contextMenu.Items.Add("Саундпад…", null, delegate { AppModes.Launch(AppMode.Soundpad); }); 84 contextMenu.Items.Add("Поиск…", null, delegate { AppModes.Launch(AppMode.Search); }); 85 } 86 else if (mode == AppMode.TextAssist) 87 { 88 cmode = ControllerMode.TextAssist; 89 WindowCapture.Helpers.SageClient.WarmUp(); // preload SAGE so first Ctrl+Shift+Space is fast 90 if (Settings.ContextNnRescore) WindowCapture.Helpers.RescoreClient.WarmUp(); // preload the context model 91 if (Settings.TsfBridge) WindowCapture.Helpers.TipBridge.Start(); // localhost bridge for the native TSF TIP 92 93 AddAutocorrectToggles(contextMenu.Items); 94 } 95 else if (mode == AppMode.Clipboard) 96 { 97 cmode = ControllerMode.Clipboard; 98 ClipboardForm.InstallListener(); 99 100 var miClipboard = new ToolStripMenuItem("История буфера (Ctrl+V удержание)") { Checked = Settings.ClipboardHistory, CheckOnClick = true }; 101 miClipboard.Click += delegate { Settings.ClipboardHistory = miClipboard.Checked; Settings.Save(); }; 102 contextMenu.Items.Add(miClipboard); 103 } 104 else // Screenshot (capture + editor only) 105 { 106 cmode = ControllerMode.Capture; 107 } 108 109 controller = new Controller(cmode); 110 controller.StatusChanged += Controller_StatusChanged; 111 112 // Unified app: apply persisted module on/off (all hooks already installed; gate behavior). 113 if (mode == AppMode.MediaCore) 114 { 115 controller.SetModuleEnabled(ControllerMode.Capture, Settings.ModuleScreenshot); 116 controller.SetModuleEnabled(ControllerMode.TextAssist, Settings.ModuleTextAssist); 117 controller.SetModuleEnabled(ControllerMode.Clipboard, Settings.ModuleClipboard); 118 controller.SetModuleEnabled(ControllerMode.Gestures, Settings.ModuleGestures); 119 } 120 121 contextMenu.Items.Add("-"); 122 contextMenu.Items.Add("Настройки", null, OnSettings); 123 contextMenu.Items.Add("О программе", null, OnAbout); 124 contextMenu.Items.Add("-"); 125 contextMenu.Items.Add("Выход", null, OnExit); 126 127 // Create tray icon (mode-colored disc so the separate apps are distinguishable) 128 trayIcon = new NotifyIcon(); 129 trayIcon.Icon = AppModes.CreateDiscIcon(AppModes.Accent(mode)); 130 trayIcon.ContextMenuStrip = contextMenu; 131 trayIcon.Visible = true; 132 trayIcon.Text = mode == AppMode.MediaCore ? "MediaCore" 133 : mode == AppMode.Screenshot ? "MediaCore — Скриншот (" + Settings.ActivationKeyName + "+Win)" 134 : "MediaCore — " + AppModes.Title(mode); 135 136 trayIcon.DoubleClick += delegate { OnSettings(null, EventArgs.Empty); }; 137 } 138 139 /// <summary>Live enable/disable of a module on the running controller (unified app).</summary> 140 private void ApplyModule(ControllerMode bit, bool on) 141 { 142 if (controller != null) controller.SetModuleEnabled(bit, on); 143 } 144 145 /// <summary>Adds the autocorrect sub-toggles (T9, layout, caps, punctuation, SAGE, rubert-tiny2, 146 /// TSF bridge) to a menu collection — shared by the standalone Автозамена app and the unified 147 /// MediaCore app's "Автозамена" submenu.</summary> 148 private void AddAutocorrectToggles(ToolStripItemCollection items) 149 { 150 var miAutoCorrect = new ToolStripMenuItem("Автозамена (T9)") { Checked = Settings.AutoT9, CheckOnClick = true }; 151 miAutoCorrect.Click += delegate { Settings.AutoT9 = miAutoCorrect.Checked; Settings.Save(); }; 152 var miAutoLang = new ToolStripMenuItem("Смена раскладки (EN↔RU)") { Checked = Settings.AutoLangSwitch, CheckOnClick = true }; 153 miAutoLang.Click += delegate { Settings.AutoLangSwitch = miAutoLang.Checked; Settings.Save(); }; 154 var miAutoCap = new ToolStripMenuItem("Авто-заглавная") { Checked = Settings.AutoCapitalize, CheckOnClick = true }; 155 miAutoCap.Click += delegate { Settings.AutoCapitalize = miAutoCap.Checked; Settings.Save(); }; 156 var miAutoPunct = new ToolStripMenuItem("Авто-пунктуация") { Checked = Settings.AutoPunctuation, CheckOnClick = true }; 157 miAutoPunct.Click += delegate { Settings.AutoPunctuation = miAutoPunct.Checked; Settings.Save(); }; 158 var miTooltip = new ToolStripMenuItem("Подсказки коррекции") { Checked = Settings.T9ShowTooltip, CheckOnClick = true }; 159 miTooltip.Click += delegate { Settings.T9ShowTooltip = miTooltip.Checked; Settings.Save(); }; 160 var miSentenceAI = new ToolStripMenuItem("SAGE-подсказка предложения (live)") { Checked = Settings.SentenceAiSuggest, CheckOnClick = true }; 161 miSentenceAI.Click += delegate { Settings.SentenceAiSuggest = miSentenceAI.Checked; Settings.Save(); if (Settings.SentenceAiSuggest) WindowCapture.Helpers.SageClient.WarmUp(); }; 162 var miSentenceAuto = new ToolStripMenuItem("SAGE авто-замена предложения (эксп.)") { Checked = Settings.SentenceAiAutoApply, CheckOnClick = true }; 163 miSentenceAuto.Click += delegate { Settings.SentenceAiAutoApply = miSentenceAuto.Checked; Settings.Save(); if (Settings.SentenceAiAutoApply) WindowCapture.Helpers.SageClient.WarmUp(); }; 164 var miCtxNn = new ToolStripMenuItem("Контекстная нейро-правка слов (rubert-tiny2, эксп.)") { Checked = Settings.ContextNnRescore, CheckOnClick = true }; 165 miCtxNn.Click += delegate { Settings.ContextNnRescore = miCtxNn.Checked; Settings.Save(); if (Settings.ContextNnRescore) WindowCapture.Helpers.RescoreClient.WarmUp(); }; 166 var miTsfBridge = new ToolStripMenuItem("TSF-мост для in-place правки (эксп.)") { Checked = Settings.TsfBridge, CheckOnClick = true }; 167 miTsfBridge.Click += delegate { Settings.TsfBridge = miTsfBridge.Checked; Settings.Save(); if (Settings.TsfBridge) WindowCapture.Helpers.TipBridge.Start(); else WindowCapture.Helpers.TipBridge.Stop(); }; 168 169 items.Add(miAutoCorrect); 170 items.Add(miAutoLang); 171 items.Add(miAutoCap); 172 items.Add(miAutoPunct); 173 items.Add(miTooltip); 174 items.Add(miSentenceAI); 175 items.Add(miSentenceAuto); 176 items.Add(miCtxNn); 177 items.Add(miTsfBridge); 178 } 179 180 [DllImport("user32.dll", SetLastError = true)] 181 private static extern bool DestroyIcon(IntPtr hIcon); 182 183 private Icon CreateIcon() 184 { 185 // Create a simple icon programmatically 186 using (var bmp = new Bitmap(32, 32)) 187 using (var g = Graphics.FromImage(bmp)) 188 { 189 g.Clear(Color.Transparent); 190 191 // Draw a camera/capture icon 192 using (var pen = new Pen(Color.FromArgb(0, 120, 215), 2)) 193 using (var brush = new SolidBrush(Color.FromArgb(0, 120, 215))) 194 { 195 // Outer frame 196 g.DrawRectangle(pen, 4, 8, 24, 18); 197 198 // Lens circle 199 g.FillEllipse(brush, 11, 12, 10, 10); 200 201 // Flash 202 g.FillRectangle(brush, 20, 4, 6, 4); 203 } 204 205 // GetHicon() returns an HICON the caller owns. Icon.FromHandle does NOT free it, 206 // so clone into a self-owned managed Icon and destroy the native handle to avoid a GDI leak. 207 IntPtr hIcon = bmp.GetHicon(); 208 try 209 { 210 using (var tmp = Icon.FromHandle(hIcon)) 211 return (Icon)tmp.Clone(); 212 } 213 finally { DestroyIcon(hIcon); } 214 } 215 } 216 217 private void Controller_StatusChanged(string status) 218 { 219 if (trayIcon != null) 220 { 221 trayIcon.Text = "WindowCapture\n" + status; 222 } 223 } 224 225 /// <summary> 226 /// Show a toast notification in bottom-right corner 227 /// </summary> 228 public void ShowNotification(string title, string message, ToolTipIcon icon = ToolTipIcon.Info, int timeout = 2000) 229 { 230 bool isError = (icon == ToolTipIcon.Error || icon == ToolTipIcon.Warning); 231 232 // Ensure we're on UI thread 233 if (hiddenForm != null && hiddenForm.InvokeRequired) 234 { 235 hiddenForm.BeginInvoke(new Action(() => 236 { 237 ToastNotification.Show(title, message, isError); 238 })); 239 } 240 else 241 { 242 ToastNotification.Show(title, message, isError); 243 } 244 } 245 246 /// <summary> 247 /// Check if TrayApp is disposed 248 /// </summary> 249 public bool IsDisposed 250 { 251 get { return hiddenForm == null || hiddenForm.IsDisposed; } 252 } 253 254 /// <summary> 255 /// Marshal delegate to UI thread 256 /// </summary> 257 public IAsyncResult BeginInvoke(Delegate method) 258 { 259 if (hiddenForm != null && hiddenForm.IsHandleCreated && !hiddenForm.IsDisposed) 260 { 261 return hiddenForm.BeginInvoke(method); 262 } 263 return null; 264 } 265 266 private SearchForm searchForm; 267 268 public void ShowSearchPanel() 269 { 270 if (searchForm != null && !searchForm.IsDisposed) 271 { 272 searchForm.SlideIn(); 273 searchForm.Show(); 274 searchForm.Activate(); 275 return; 276 } 277 searchForm = new SearchForm(); 278 searchForm.FormClosed += (s, ev) => { searchForm = null; controller.ResetSearchOpen(); }; 279 searchForm.PanelHidden += () => controller.ResetSearchOpen(); 280 searchForm.Show(); 281 } 282 283 public void HideSearchPanel() 284 { 285 if (searchForm != null && !searchForm.IsDisposed) 286 searchForm.SlideOut(); 287 } 288 289 public void WidenSearchPanel(int extra) 290 { 291 if (searchForm != null && !searchForm.IsDisposed) 292 searchForm.SetExtraWidth(extra); 293 } 294 295 private SoundpadForm soundpadForm; 296 297 public void ShowSoundpad() 298 { 299 if (soundpadForm != null && !soundpadForm.IsDisposed) 300 { 301 soundpadForm.SlideIn(); 302 soundpadForm.Show(); 303 soundpadForm.Activate(); 304 return; 305 } 306 soundpadForm = new SoundpadForm(); 307 soundpadForm.Show(); 308 } 309 310 private void OnSettings(object sender, EventArgs e) 311 { 312 controller.ShowSettings(); 313 } 314 315 private void OnAbout(object sender, EventArgs e) 316 { 317 MessageBox.Show( 318 "MediaCore v3.0 (бывш. WindowCapture)\n\n" + 319 "Скриншоты, автозамена, буфер, саундпад, поиск — модули включаются в меню трея.\n\n" + 320 "Controls:\n" + 321 "- " + Settings.ActivationKeyName + " + Win - Freeze screen and start capture\n" + 322 "- " + Settings.ActivationKeyName + " + Win + Ctrl - Start video recording\n" + 323 "- LMB drag - Draw highlight rectangle\n" + 324 "- RMB drag - Draw arrow\n" + 325 "- MMB click - Place numbered marker\n" + 326 "- Alt + LMB drag - Auto-detect and union regions\n" + 327 "- Alt + MMB - Select window by API\n" + 328 "- Double LMB - Add text / Toggle blur on highlight\n" + 329 "- Double RMB - Add comment bubble\n" + 330 "- Ctrl + Mouse wheel - Zoom\n" + 331 "- Space - Copy to clipboard and close\n" + 332 "- Ctrl + Z - Undo\n" + 333 "- Ctrl + S - Save to file\n" + 334 "- Escape - Cancel", 335 "О программе MediaCore", 336 MessageBoxButtons.OK, 337 MessageBoxIcon.Information 338 ); 339 } 340 341 private void OnExit(object sender, EventArgs e) 342 { 343 trayIcon.Visible = false; 344 if (controller != null) controller.Dispose(); 345 Application.Exit(); 346 } 347 348 protected override void Dispose(bool disposing) 349 { 350 if (disposing) 351 { 352 if (trayIcon != null) trayIcon.Dispose(); 353 if (controller != null) controller.Dispose(); 354 if (contextMenu != null) contextMenu.Dispose(); 355 if (hiddenForm != null) hiddenForm.Dispose(); 356 } 357 base.Dispose(disposing); 358 } 359 } 360}