windowcapture
исходный код / App/TrayApp.cs

TrayApp.cs

360 строк · 17,474 байт · модуль App
  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}