|
| 1 | +// |
| 2 | +// FILE: CultureManager.cs. |
| 3 | +// |
| 4 | +// COPYRIGHT: Copyright 2008 |
| 5 | +// Infralution |
| 6 | +// |
| 7 | +using System; |
| 8 | +using System.Collections.Generic; |
| 9 | +using System.Globalization; |
| 10 | +using System.Threading; |
| 11 | +using System.Windows; |
| 12 | +using System.Windows.Markup; |
| 13 | +using System.Windows.Forms; |
| 14 | +using System.Reflection; |
| 15 | +using Infralution.Localization.Wpf.Properties; |
| 16 | +using System.Runtime.InteropServices; |
| 17 | +namespace Infralution.Localization.Wpf |
| 18 | +{ |
| 19 | + |
| 20 | + /// <summary> |
| 21 | + /// Provides the ability to change the UICulture for WPF Windows and controls |
| 22 | + /// dynamically. |
| 23 | + /// </summary> |
| 24 | + /// <remarks> |
| 25 | + /// XAML elements that use the <see cref="ResxExtension"/> are automatically |
| 26 | + /// updated when the <see cref="CultureManager.UICulture"/> property is changed. |
| 27 | + /// </remarks> |
| 28 | + public static class CultureManager |
| 29 | + { |
| 30 | + #region Static Member Variables |
| 31 | + |
| 32 | + /// <summary> |
| 33 | + /// Current UICulture of the application |
| 34 | + /// </summary> |
| 35 | + private static CultureInfo _uiCulture; |
| 36 | + |
| 37 | + /// <summary> |
| 38 | + /// The active design time culture selection window (if any) |
| 39 | + /// </summary> |
| 40 | + private static CultureSelectWindow _cultureSelectWindow; |
| 41 | + |
| 42 | + /// <summary> |
| 43 | + /// The active task bar notify icon for design time culture selection (if any) |
| 44 | + /// </summary> |
| 45 | + private static NotifyIcon _notifyIcon; |
| 46 | + |
| 47 | + /// <summary> |
| 48 | + /// The window handle for the notify icon |
| 49 | + /// </summary> |
| 50 | + private static IntPtr _notifyIconHandle; |
| 51 | + |
| 52 | + /// <summary> |
| 53 | + /// Should the <see cref="Thread.CurrentCulture"/> be changed when the |
| 54 | + /// <see cref="UICulture"/> changes. |
| 55 | + /// </summary> |
| 56 | + private static bool _synchronizeThreadCulture = true; |
| 57 | + |
| 58 | + #endregion |
| 59 | + |
| 60 | + #region Public Interface |
| 61 | + |
| 62 | + /// <summary> |
| 63 | + /// Raised when the <see cref="UICulture"/> is changed |
| 64 | + /// </summary> |
| 65 | + /// <remarks> |
| 66 | + /// Since this event is static if the client object does not detach from the event a reference |
| 67 | + /// will be maintained to the client object preventing it from being garbage collected - thus |
| 68 | + /// causing a potential memory leak. |
| 69 | + /// </remarks> |
| 70 | + public static event EventHandler UICultureChanged; |
| 71 | + |
| 72 | + /// <summary> |
| 73 | + /// Sets the UICulture for the WPF application and raises the <see cref="UICultureChanged"/> |
| 74 | + /// event causing any XAML elements using the <see cref="ResxExtension"/> to automatically |
| 75 | + /// update |
| 76 | + /// </summary> |
| 77 | + public static CultureInfo UICulture |
| 78 | + { |
| 79 | + get |
| 80 | + { |
| 81 | + if (_uiCulture == null) |
| 82 | + { |
| 83 | + _uiCulture = Thread.CurrentThread.CurrentUICulture; |
| 84 | + } |
| 85 | + return _uiCulture; |
| 86 | + } |
| 87 | + set |
| 88 | + { |
| 89 | + if (value != UICulture) |
| 90 | + { |
| 91 | + _uiCulture = value; |
| 92 | + Thread.CurrentThread.CurrentUICulture = value; |
| 93 | + if (SynchronizeThreadCulture) |
| 94 | + { |
| 95 | + SetThreadCulture(value); |
| 96 | + } |
| 97 | + UICultureExtension.UpdateAllTargets(); |
| 98 | + ResxExtension.UpdateAllTargets(); |
| 99 | + if (UICultureChanged != null) |
| 100 | + { |
| 101 | + UICultureChanged(null, EventArgs.Empty); |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + /// <summary> |
| 108 | + /// If set to true then the <see cref="Thread.CurrentCulture"/> property is changed |
| 109 | + /// to match the current <see cref="UICulture"/> |
| 110 | + /// </summary> |
| 111 | + public static bool SynchronizeThreadCulture |
| 112 | + { |
| 113 | + get { return _synchronizeThreadCulture; } |
| 114 | + set |
| 115 | + { |
| 116 | + _synchronizeThreadCulture = value; |
| 117 | + if (value) |
| 118 | + { |
| 119 | + SetThreadCulture(UICulture); |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + #endregion |
| 125 | + |
| 126 | + #region Local Methods |
| 127 | + |
| 128 | + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] |
| 129 | + private class NOTIFYICONDATA |
| 130 | + { |
| 131 | + public int cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA)); |
| 132 | + public IntPtr hWnd; |
| 133 | + public int uID; |
| 134 | + public int uFlags; |
| 135 | + public int uCallbackMessage; |
| 136 | + public IntPtr hIcon; |
| 137 | + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] |
| 138 | + public string szTip; |
| 139 | + public int dwState; |
| 140 | + public int dwStateMask; |
| 141 | + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] |
| 142 | + public string szInfo; |
| 143 | + public int uTimeoutOrVersion; |
| 144 | + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] |
| 145 | + public string szInfoTitle; |
| 146 | + public int dwInfoFlags; |
| 147 | + } |
| 148 | + |
| 149 | + [DllImport("shell32.dll", CharSet = CharSet.Auto)] |
| 150 | + private static extern int Shell_NotifyIcon(int message, NOTIFYICONDATA pnid); |
| 151 | + |
| 152 | + /// <summary> |
| 153 | + /// Set the thread culture to the given culture |
| 154 | + /// </summary> |
| 155 | + /// <param name="value">The culture to set</param> |
| 156 | + /// <remarks>If the culture is neutral then creates a specific culture</remarks> |
| 157 | + private static void SetThreadCulture(CultureInfo value) |
| 158 | + { |
| 159 | + if (value.IsNeutralCulture) |
| 160 | + { |
| 161 | + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(value.Name); |
| 162 | + } |
| 163 | + else |
| 164 | + { |
| 165 | + Thread.CurrentThread.CurrentCulture = value; |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + /// <summary> |
| 170 | + /// Show the UICultureSelector to allow selection of the active UI culture |
| 171 | + /// </summary> |
| 172 | + internal static void ShowCultureNotifyIcon() |
| 173 | + { |
| 174 | + if (_notifyIcon == null) |
| 175 | + { |
| 176 | + ToolStripMenuItem menuItem; |
| 177 | + |
| 178 | + _notifyIcon = new NotifyIcon(); |
| 179 | + _notifyIcon.Icon = Resources.UICultureIcon; |
| 180 | + _notifyIcon.MouseClick += new MouseEventHandler(OnCultureNotifyIconMouseClick); |
| 181 | + _notifyIcon.MouseDoubleClick += new MouseEventHandler(OnCultureNotifyIconMouseDoubleClick); |
| 182 | + _notifyIcon.Text = Resources.UICultureSelectText; |
| 183 | + ContextMenuStrip menuStrip = new ContextMenuStrip(); |
| 184 | + |
| 185 | + // separator |
| 186 | + // |
| 187 | + menuStrip.Items.Add(new ToolStripSeparator()); |
| 188 | + |
| 189 | + // add menu to open culture select window |
| 190 | + // |
| 191 | + menuItem = new ToolStripMenuItem(Resources.OtherCulturesMenu); |
| 192 | + menuItem.Click += new EventHandler(OnCultureSelectMenuClick); |
| 193 | + menuStrip.Items.Add(menuItem); |
| 194 | + |
| 195 | + menuStrip.Opening += OnMenuStripOpening; |
| 196 | + _notifyIcon.ContextMenuStrip = menuStrip; |
| 197 | + _notifyIcon.Visible = true; |
| 198 | + |
| 199 | + // Save the window handle associated with the notify icon - note that the window |
| 200 | + // is destroyed before the ProcessExit event gets called so calling NotifyIcon.Dispose |
| 201 | + // within the ProcessExit event handler doesn't work because the window handle has been |
| 202 | + // set to zero by that stage |
| 203 | + // |
| 204 | + FieldInfo fieldInfo = typeof(NotifyIcon).GetField("window", BindingFlags.Instance | BindingFlags.NonPublic); |
| 205 | + if (fieldInfo != null) |
| 206 | + { |
| 207 | + NativeWindow iconWindow = fieldInfo.GetValue(_notifyIcon) as NativeWindow; |
| 208 | + _notifyIconHandle = iconWindow.Handle; |
| 209 | + } |
| 210 | + |
| 211 | + AppDomain.CurrentDomain.ProcessExit += OnDesignerExit; |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + /// <summary> |
| 216 | + /// Remove the culture notify icon when the designer process exits |
| 217 | + /// </summary> |
| 218 | + /// <param name="sender"></param> |
| 219 | + /// <param name="e"></param> |
| 220 | + private static void OnDesignerExit(object sender, EventArgs e) |
| 221 | + { |
| 222 | + // By the time the ProcessExit event is called the window associated with the |
| 223 | + // notify icon has been destroyed - and a bug in the NotifyIcon class means that |
| 224 | + // the notify icon is not removed. This works around the issue by saving the |
| 225 | + // window handle when the NotifyIcon is created and then calling the Shell_NotifyIcon |
| 226 | + // method ourselves to remove the icon from the tray |
| 227 | + // |
| 228 | + if (_notifyIconHandle != IntPtr.Zero) |
| 229 | + { |
| 230 | + NOTIFYICONDATA iconData = new NOTIFYICONDATA(); |
| 231 | + iconData.uCallbackMessage = 2048; |
| 232 | + iconData.uFlags = 1; |
| 233 | + iconData.hWnd = _notifyIconHandle; |
| 234 | + iconData.uID = 1; |
| 235 | + iconData.hIcon = IntPtr.Zero; |
| 236 | + iconData.szTip = null; |
| 237 | + Shell_NotifyIcon(2, iconData); |
| 238 | + } |
| 239 | + } |
| 240 | + |
| 241 | + /// <summary> |
| 242 | + /// Display the CultureSelectWindow to allow the user to select the UICulture |
| 243 | + /// </summary> |
| 244 | + private static void DisplayCultureSelectWindow() |
| 245 | + { |
| 246 | + if (_cultureSelectWindow == null) |
| 247 | + { |
| 248 | + _cultureSelectWindow = new CultureSelectWindow(); |
| 249 | + _cultureSelectWindow.Title = _notifyIcon.Text; |
| 250 | + _cultureSelectWindow.Closed += new EventHandler(OnCultureSelectWindowClosed); |
| 251 | + _cultureSelectWindow.Show(); |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + /// <summary> |
| 256 | + /// Is there already an entry for the culture in the context menu |
| 257 | + /// </summary> |
| 258 | + /// <param name="culture">The culture to check</param> |
| 259 | + /// <returns>True if there is a menu</returns> |
| 260 | + private static bool CultureMenuExists(CultureInfo culture) |
| 261 | + { |
| 262 | + foreach (ToolStripItem item in _notifyIcon.ContextMenuStrip.Items) |
| 263 | + { |
| 264 | + CultureInfo itemCulture = item.Tag as CultureInfo; |
| 265 | + if (itemCulture != null && itemCulture.Name == culture.Name) |
| 266 | + { |
| 267 | + return true; |
| 268 | + } |
| 269 | + } |
| 270 | + return false; |
| 271 | + } |
| 272 | + |
| 273 | + /// <summary> |
| 274 | + /// Add a menu item to the NotifyIcon for the current UICulture |
| 275 | + /// </summary> |
| 276 | + /// <param name="culture"></param> |
| 277 | + private static void AddCultureMenuItem(CultureInfo culture) |
| 278 | + { |
| 279 | + if (!CultureMenuExists(culture)) |
| 280 | + { |
| 281 | + ContextMenuStrip menuStrip = _notifyIcon.ContextMenuStrip; |
| 282 | + ToolStripMenuItem menuItem = new ToolStripMenuItem(culture.DisplayName); |
| 283 | + menuItem.Checked = true; |
| 284 | + menuItem.CheckOnClick = true; |
| 285 | + menuItem.Tag = culture; |
| 286 | + menuItem.CheckedChanged += new EventHandler(OnCultureMenuCheckChanged); |
| 287 | + menuStrip.Items.Insert(menuStrip.Items.Count - 2, menuItem); |
| 288 | + } |
| 289 | + } |
| 290 | + |
| 291 | + /// <summary> |
| 292 | + /// Update the notify icon menu |
| 293 | + /// </summary> |
| 294 | + private static void OnMenuStripOpening(object sender, System.ComponentModel.CancelEventArgs e) |
| 295 | + { |
| 296 | + |
| 297 | + // ensure the current culture is always on the menu |
| 298 | + // |
| 299 | + AddCultureMenuItem(UICulture); |
| 300 | + |
| 301 | + // Add the design time cultures |
| 302 | + // |
| 303 | + List<CultureInfo> designTimeCultures = ResxExtension.GetDesignTimeCultures(); |
| 304 | + foreach (CultureInfo culture in designTimeCultures) |
| 305 | + { |
| 306 | + AddCultureMenuItem(culture); |
| 307 | + } |
| 308 | + |
| 309 | + ContextMenuStrip menuStrip = _notifyIcon.ContextMenuStrip; |
| 310 | + foreach (ToolStripItem item in menuStrip.Items) |
| 311 | + { |
| 312 | + ToolStripMenuItem menuItem = item as ToolStripMenuItem; |
| 313 | + if (menuItem != null) |
| 314 | + { |
| 315 | + menuItem.Checked = (menuItem.Tag == UICulture); |
| 316 | + } |
| 317 | + } |
| 318 | + } |
| 319 | + |
| 320 | + /// <summary> |
| 321 | + /// Display the context menu for left clicks (right clicks are handled automatically) |
| 322 | + /// </summary> |
| 323 | + /// <param name="sender"></param> |
| 324 | + /// <param name="e"></param> |
| 325 | + private static void OnCultureNotifyIconMouseClick(object sender, MouseEventArgs e) |
| 326 | + { |
| 327 | + if (e.Button == MouseButtons.Left) |
| 328 | + { |
| 329 | + MethodInfo methodInfo = typeof(NotifyIcon).GetMethod("ShowContextMenu", |
| 330 | + BindingFlags.Instance | BindingFlags.NonPublic); |
| 331 | + methodInfo.Invoke(_notifyIcon, null); |
| 332 | + } |
| 333 | + } |
| 334 | + |
| 335 | + /// <summary> |
| 336 | + /// Display the CultureSelectWindow when the user double clicks on the NotifyIcon |
| 337 | + /// </summary> |
| 338 | + /// <param name="sender"></param> |
| 339 | + /// <param name="e"></param> |
| 340 | + private static void OnCultureNotifyIconMouseDoubleClick(object sender, MouseEventArgs e) |
| 341 | + { |
| 342 | + DisplayCultureSelectWindow(); |
| 343 | + } |
| 344 | + |
| 345 | + /// <summary> |
| 346 | + /// Display the CultureSelectWindow when the user selects the menu option |
| 347 | + /// </summary> |
| 348 | + /// <param name="sender"></param> |
| 349 | + /// <param name="e"></param> |
| 350 | + private static void OnCultureSelectMenuClick(object sender, EventArgs e) |
| 351 | + { |
| 352 | + DisplayCultureSelectWindow(); |
| 353 | + } |
| 354 | + |
| 355 | + /// <summary> |
| 356 | + /// Handle change of culture via the NotifyIcon menu |
| 357 | + /// </summary> |
| 358 | + /// <param name="sender"></param> |
| 359 | + /// <param name="e"></param> |
| 360 | + private static void OnCultureMenuCheckChanged(object sender, EventArgs e) |
| 361 | + { |
| 362 | + ToolStripMenuItem menuItem = sender as ToolStripMenuItem; |
| 363 | + if (menuItem.Checked) |
| 364 | + { |
| 365 | + UICulture = menuItem.Tag as CultureInfo; |
| 366 | + } |
| 367 | + } |
| 368 | + |
| 369 | + /// <summary> |
| 370 | + /// Handle close of the culture select window |
| 371 | + /// </summary> |
| 372 | + /// <param name="sender"></param> |
| 373 | + /// <param name="e"></param> |
| 374 | + private static void OnCultureSelectWindowClosed(object sender, EventArgs e) |
| 375 | + { |
| 376 | + _cultureSelectWindow = null; |
| 377 | + } |
| 378 | + |
| 379 | + #endregion |
| 380 | + |
| 381 | + } |
| 382 | + |
| 383 | +} |
0 commit comments