diff --git a/EarTrumpet.Interop/AudioSessionService.cpp b/EarTrumpet.Interop/AudioSessionService.cpp index 8f638387e..dbad07d44 100644 --- a/EarTrumpet.Interop/AudioSessionService.cpp +++ b/EarTrumpet.Interop/AudioSessionService.cpp @@ -16,102 +16,103 @@ AudioSessionService* AudioSessionService::__instance = nullptr; void AudioSessionService::CleanUpAudioSessions() { - for (auto session = _sessions.begin(); session != _sessions.end(); session++) - { - CoTaskMemFree(session->DisplayName); - CoTaskMemFree(session->IconPath); - } - - _sessions.clear(); - _sessionMap.clear(); + for (auto session = _sessions.begin(); session != _sessions.end(); session++) + { + CoTaskMemFree(session->DisplayName); + CoTaskMemFree(session->IconPath); + } + + _sessions.clear(); + _sessionMap.clear(); } int AudioSessionService::GetAudioSessionCount() { - return _sessions.size(); + return _sessions.size(); } HRESULT AudioSessionService::RefreshAudioSessions() { - CleanUpAudioSessions(); + CleanUpAudioSessions(); - CComPtr deviceEnumerator; - FAST_FAIL(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&deviceEnumerator))); + CComPtr deviceEnumerator; + FAST_FAIL(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&deviceEnumerator))); - // TIP: Role parameter is not actually used https://msdn.microsoft.com/en-us/library/windows/desktop/dd371401.aspx - CComPtr device; - FAST_FAIL(deviceEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eMultimedia, &device)); + // TIP: Role parameter is not actually used https://msdn.microsoft.com/en-us/library/windows/desktop/dd371401.aspx + CComPtr device; + FAST_FAIL(deviceEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eMultimedia, &device)); - CComPtr audioSessionManager; - FAST_FAIL(device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC, nullptr, (void**)&audioSessionManager)); + CComPtr audioSessionManager; + FAST_FAIL(device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_INPROC, nullptr, (void**)&audioSessionManager)); - CComPtr audioSessionEnumerator; - FAST_FAIL(audioSessionManager->GetSessionEnumerator(&audioSessionEnumerator)); + CComPtr audioSessionEnumerator; + FAST_FAIL(audioSessionManager->GetSessionEnumerator(&audioSessionEnumerator)); - int sessionCount; - FAST_FAIL(audioSessionEnumerator->GetCount(&sessionCount)); + int sessionCount; + FAST_FAIL(audioSessionEnumerator->GetCount(&sessionCount)); - for (int i = 0; i < sessionCount; i++) - { - EarTrumpetAudioSession audioSession; - if (SUCCEEDED(CreateEtAudioSessionFromAudioSession(audioSessionEnumerator, i, &audioSession))) - { - _sessions.push_back(audioSession); - } - } + for (int i = 0; i < sessionCount; i++) + { + EarTrumpetAudioSession audioSession; + if (SUCCEEDED(CreateEtAudioSessionFromAudioSession(audioSessionEnumerator, i, &audioSession))) + { + _sessions.push_back(audioSession); + } + } - return S_OK; + return S_OK; } HRESULT AudioSessionService::CreateEtAudioSessionFromAudioSession(CComPtr audioSessionEnumerator, int sessionCount, EarTrumpetAudioSession* etAudioSession) { - CComPtr audioSessionControl; - FAST_FAIL(audioSessionEnumerator->GetSession(sessionCount, &audioSessionControl)); + CComPtr audioSessionControl; + FAST_FAIL(audioSessionEnumerator->GetSession(sessionCount, &audioSessionControl)); - CComPtr audioSessionControl2; - FAST_FAIL(audioSessionControl->QueryInterface(IID_PPV_ARGS(&audioSessionControl2))); + CComPtr audioSessionControl2; + FAST_FAIL(audioSessionControl->QueryInterface(IID_PPV_ARGS(&audioSessionControl2))); - DWORD pid; - FAST_FAIL(audioSessionControl2->GetProcessId(&pid)); + DWORD pid; + FAST_FAIL(audioSessionControl2->GetProcessId(&pid)); etAudioSession->ProcessId = pid; FAST_FAIL(audioSessionControl2->GetGroupingParam(&etAudioSession->GroupingId)); - CComHeapPtr sessionIdString; - FAST_FAIL(audioSessionControl2->GetSessionIdentifier(&sessionIdString)); + CComHeapPtr sessionIdString; + FAST_FAIL(audioSessionControl2->GetSessionIdentifier(&sessionIdString)); - hash stringHash; - etAudioSession->SessionId = stringHash(static_cast(sessionIdString)); + hash stringHash; + etAudioSession->SessionId = stringHash(static_cast(sessionIdString)); - _sessionMap[etAudioSession->SessionId] = audioSessionControl2; + _sessionMap[etAudioSession->SessionId] = audioSessionControl2; - CComPtr simpleAudioVolume; - FAST_FAIL(audioSessionControl->QueryInterface(IID_PPV_ARGS(&simpleAudioVolume))); - FAST_FAIL(simpleAudioVolume->GetMasterVolume(&etAudioSession->Volume)); + CComPtr simpleAudioVolume; + FAST_FAIL(audioSessionControl->QueryInterface(IID_PPV_ARGS(&simpleAudioVolume))); + FAST_FAIL(simpleAudioVolume->GetMasterVolume(&etAudioSession->Volume)); - if (IsImmersiveProcess(pid)) - { - PWSTR appUserModelId; - FAST_FAIL(GetAppUserModelIdFromPid(pid, &appUserModelId)); + HRESULT hr = IsImmersiveProcess(pid); + if (hr == S_OK) + { + PWSTR appUserModelId; + FAST_FAIL(GetAppUserModelIdFromPid(pid, &appUserModelId)); - FAST_FAIL(GetAppProperties(appUserModelId, &etAudioSession->DisplayName, &etAudioSession->IconPath, &etAudioSession->BackgroundColor)); + FAST_FAIL(GetAppProperties(appUserModelId, &etAudioSession->DisplayName, &etAudioSession->IconPath, &etAudioSession->BackgroundColor)); - etAudioSession->IsDesktopApp = false; - } - else - { + etAudioSession->IsDesktopApp = false; + } + else if (hr == S_FALSE) + { bool isSystemSoundsSession = (S_OK == audioSessionControl2->IsSystemSoundsSession()); - AudioSessionState state; - FAST_FAIL(audioSessionControl2->GetState(&state)); - if (!isSystemSoundsSession && (state == AudioSessionState::AudioSessionStateExpired)) - { - return E_NOT_VALID_STATE; - } - - if (isSystemSoundsSession) - { + AudioSessionState state; + FAST_FAIL(audioSessionControl2->GetState(&state)); + if (!isSystemSoundsSession && (state == AudioSessionState::AudioSessionStateExpired)) + { + return E_NOT_VALID_STATE; + } + + if (isSystemSoundsSession) + { PCWSTR pszDllPath; BOOL isWow64Process; if (!IsWow64Process(GetCurrentProcess(), &isWow64Process) || isWow64Process) @@ -131,115 +132,114 @@ HRESULT AudioSessionService::CreateEtAudioSessionFromAudioSession(CComPtrIconPath)); FAST_FAIL(SHStrDup(L"System Sounds", &etAudioSession->DisplayName)); - } - else - { - shared_ptr processHandle(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid), CloseHandle); - FAST_FAIL_HANDLE(processHandle.get()); - - wchar_t imagePath[MAX_PATH] = {}; - DWORD dwCch = ARRAYSIZE(imagePath); + } + else + { + shared_ptr processHandle(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid), CloseHandle); + FAST_FAIL_HANDLE(processHandle.get()); + + wchar_t imagePath[MAX_PATH] = {}; + DWORD dwCch = ARRAYSIZE(imagePath); FAST_FAIL(QueryFullProcessImageName(processHandle.get(), 0, imagePath, &dwCch) == 0 ? E_FAIL : S_OK); FAST_FAIL(SHStrDup(imagePath, &etAudioSession->IconPath)); FAST_FAIL(SHStrDup(PathFindFileName(imagePath), &etAudioSession->DisplayName)); - } + } - etAudioSession->IsDesktopApp = true; - etAudioSession->BackgroundColor = 0x00000000; - } + etAudioSession->IsDesktopApp = true; + etAudioSession->BackgroundColor = 0x00000000; + } - return S_OK; + return S_OK; } HRESULT AudioSessionService::GetAudioSessions(void** audioSessions) { - if (_sessions.size() == 0) - { - return HRESULT_FROM_WIN32(ERROR_NO_MORE_ITEMS); - } + if (_sessions.size() == 0) + { + return HRESULT_FROM_WIN32(ERROR_NO_MORE_ITEMS); + } - *audioSessions = &_sessions[0]; - return S_OK; + *audioSessions = &_sessions[0]; + return S_OK; } -BOOL AudioSessionService::IsImmersiveProcess(DWORD pid) +HRESULT AudioSessionService::IsImmersiveProcess(DWORD pid) { - shared_ptr processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid), CloseHandle); - FAST_FAIL_HANDLE(processHandle.get()); - - return ::IsImmersiveProcess(processHandle.get()); + shared_ptr processHandle(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid), CloseHandle); + FAST_FAIL_HANDLE(processHandle.get()); + return (::IsImmersiveProcess(processHandle.get()) ? S_OK : S_FALSE); } HRESULT AudioSessionService::GetAppUserModelIdFromPid(DWORD pid, LPWSTR* applicationUserModelIdPtr) { - shared_ptr processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid), CloseHandle); - FAST_FAIL_HANDLE(processHandle.get()); - - unsigned int appUserModelIdLength = 0; - long returnCode = GetApplicationUserModelId(processHandle.get(), &appUserModelIdLength, NULL); - if (returnCode != ERROR_INSUFFICIENT_BUFFER) - { - return HRESULT_FROM_WIN32(returnCode); - } - - unique_ptr appUserModelId(new wchar_t[appUserModelIdLength]); - returnCode = GetApplicationUserModelId(processHandle.get(), &appUserModelIdLength, appUserModelId.get()); - if (returnCode != ERROR_SUCCESS) - { - return HRESULT_FROM_WIN32(returnCode); - } - - FAST_FAIL(SHStrDup(appUserModelId.get(), applicationUserModelIdPtr)); - - return S_OK; + shared_ptr processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid), CloseHandle); + FAST_FAIL_HANDLE(processHandle.get()); + + unsigned int appUserModelIdLength = 0; + long returnCode = GetApplicationUserModelId(processHandle.get(), &appUserModelIdLength, NULL); + if (returnCode != ERROR_INSUFFICIENT_BUFFER) + { + return HRESULT_FROM_WIN32(returnCode); + } + + unique_ptr appUserModelId(new wchar_t[appUserModelIdLength]); + returnCode = GetApplicationUserModelId(processHandle.get(), &appUserModelIdLength, appUserModelId.get()); + if (returnCode != ERROR_SUCCESS) + { + return HRESULT_FROM_WIN32(returnCode); + } + + FAST_FAIL(SHStrDup(appUserModelId.get(), applicationUserModelIdPtr)); + + return S_OK; } HRESULT AudioSessionService::SetAudioSessionVolume(unsigned long sessionId, float volume) { - if (!_sessionMap[sessionId]) - { - return E_INVALIDARG; - } + if (!_sessionMap[sessionId]) + { + return E_INVALIDARG; + } - CComPtr simpleAudioVolume; - FAST_FAIL(_sessionMap[sessionId]->QueryInterface(IID_PPV_ARGS(&simpleAudioVolume))); + CComPtr simpleAudioVolume; + FAST_FAIL(_sessionMap[sessionId]->QueryInterface(IID_PPV_ARGS(&simpleAudioVolume))); - FAST_FAIL(simpleAudioVolume->SetMasterVolume(volume, nullptr)); - - return S_OK; + FAST_FAIL(simpleAudioVolume->SetMasterVolume(volume, nullptr)); + + return S_OK; } HRESULT AudioSessionService::GetAppProperties(PCWSTR pszAppId, PWSTR* ppszName, PWSTR* ppszIcon, ULONG *background) { - *ppszIcon = nullptr; - *ppszName = nullptr; - *background = 0; + *ppszIcon = nullptr; + *ppszName = nullptr; + *background = 0; - CComPtr item; - FAST_FAIL(SHCreateItemInKnownFolder(FOLDERID_AppsFolder, KF_FLAG_DONT_VERIFY, pszAppId, IID_PPV_ARGS(&item))); + CComPtr item; + FAST_FAIL(SHCreateItemInKnownFolder(FOLDERID_AppsFolder, KF_FLAG_DONT_VERIFY, pszAppId, IID_PPV_ARGS(&item))); - CComHeapPtr itemName; - FAST_FAIL(item->GetString(PKEY_ItemNameDisplay, &itemName)); - FAST_FAIL(item->GetUInt32(PKEY_AppUserModel_Background, background)); + CComHeapPtr itemName; + FAST_FAIL(item->GetString(PKEY_ItemNameDisplay, &itemName)); + FAST_FAIL(item->GetUInt32(PKEY_AppUserModel_Background, background)); - CComHeapPtr installPath; - FAST_FAIL(item->GetString(PKEY_AppUserModel_PackageInstallPath, &installPath)); + CComHeapPtr installPath; + FAST_FAIL(item->GetString(PKEY_AppUserModel_PackageInstallPath, &installPath)); - CComHeapPtr iconPath; - FAST_FAIL(item->GetString(PKEY_AppUserModel_Icon, &iconPath)); + CComHeapPtr iconPath; + FAST_FAIL(item->GetString(PKEY_AppUserModel_Icon, &iconPath)); - wchar_t fullPath[MAX_PATH] = {}; - FAST_FAIL(PathCchCombine(fullPath, ARRAYSIZE(fullPath), installPath, iconPath)); + wchar_t fullPath[MAX_PATH] = {}; + FAST_FAIL(PathCchCombine(fullPath, ARRAYSIZE(fullPath), installPath, iconPath)); - CStringW path(fullPath); + CStringW path(fullPath); - if (!PathFileExists(path)) - { - path.Replace(L".png", L".scale-100.png"); - } + if (!PathFileExists(path)) + { + path.Replace(L".png", L".scale-100.png"); + } - FAST_FAIL(SHStrDup(path, ppszIcon)); - *ppszName = itemName.Detach(); - return S_OK; + FAST_FAIL(SHStrDup(path, ppszIcon)); + *ppszName = itemName.Detach(); + return S_OK; } \ No newline at end of file diff --git a/EarTrumpet.Interop/AudioSessionService.h b/EarTrumpet.Interop/AudioSessionService.h index 94ca549c9..440765043 100644 --- a/EarTrumpet.Interop/AudioSessionService.h +++ b/EarTrumpet.Interop/AudioSessionService.h @@ -2,48 +2,48 @@ namespace EarTrumpet { - namespace Interop - { - struct EarTrumpetAudioSession - { - wchar_t* DisplayName; - wchar_t* IconPath; + namespace Interop + { + struct EarTrumpetAudioSession + { + wchar_t* DisplayName; + wchar_t* IconPath; GUID GroupingId; - unsigned long SessionId; - unsigned long ProcessId; - unsigned long BackgroundColor; - float Volume; + unsigned long SessionId; + unsigned long ProcessId; + unsigned long BackgroundColor; + float Volume; bool IsDesktopApp; - }; + }; - class AudioSessionService - { + class AudioSessionService + { private: - static AudioSessionService* __instance; - - void CleanUpAudioSessions(); - HRESULT CreateEtAudioSessionFromAudioSession(CComPtr sessionEnumerator, int sessionCount, EarTrumpetAudioSession* etAudioSession); - HRESULT GetAppProperties(PCWSTR pszAppId, PWSTR* ppszName, PWSTR* ppszIcon, ULONG *background); - HRESULT GetAppUserModelIdFromPid(DWORD pid, LPWSTR* applicationUserModelIdPtr); - BOOL IsImmersiveProcess(DWORD pid); + static AudioSessionService* __instance; + + void CleanUpAudioSessions(); + HRESULT CreateEtAudioSessionFromAudioSession(CComPtr sessionEnumerator, int sessionCount, EarTrumpetAudioSession* etAudioSession); + HRESULT GetAppProperties(PCWSTR pszAppId, PWSTR* ppszName, PWSTR* ppszIcon, ULONG *background); + HRESULT GetAppUserModelIdFromPid(DWORD pid, LPWSTR* applicationUserModelIdPtr); + HRESULT IsImmersiveProcess(DWORD pid); - std::vector _sessions; - std::map> _sessionMap; - + std::vector _sessions; + std::map> _sessionMap; + public: - static AudioSessionService* instance() - { - if (!__instance) - { - __instance = new AudioSessionService; - } - return __instance; - } + static AudioSessionService* instance() + { + if (!__instance) + { + __instance = new AudioSessionService; + } + return __instance; + } - int GetAudioSessionCount(); - HRESULT GetAudioSessions(void** audioSessions); - HRESULT RefreshAudioSessions(); - HRESULT SetAudioSessionVolume(unsigned long sessionId, float volume); - }; - } + int GetAudioSessionCount(); + HRESULT GetAudioSessions(void** audioSessions); + HRESULT RefreshAudioSessions(); + HRESULT SetAudioSessionVolume(unsigned long sessionId, float volume); + }; + } } \ No newline at end of file diff --git a/EarTrumpet.Interop/exports.cpp b/EarTrumpet.Interop/exports.cpp index fb4c42a98..2142a02d3 100644 --- a/EarTrumpet.Interop/exports.cpp +++ b/EarTrumpet.Interop/exports.cpp @@ -6,20 +6,20 @@ using namespace EarTrumpet::Interop; extern "C" __declspec(dllexport) HRESULT RefreshAudioSessions() { - return AudioSessionService::instance()->RefreshAudioSessions(); + return AudioSessionService::instance()->RefreshAudioSessions(); } extern "C" __declspec(dllexport) int GetAudioSessionCount() { - return AudioSessionService::instance()->GetAudioSessionCount(); + return AudioSessionService::instance()->GetAudioSessionCount(); } extern "C" __declspec(dllexport) HRESULT GetAudioSessions(void** audioSessions) { - return AudioSessionService::instance()->GetAudioSessions(audioSessions); + return AudioSessionService::instance()->GetAudioSessions(audioSessions); } extern "C" __declspec(dllexport) HRESULT SetAudioSessionVolume(unsigned long sessionId, float volume) { - return AudioSessionService::instance()->SetAudioSessionVolume(sessionId, volume); + return AudioSessionService::instance()->SetAudioSessionVolume(sessionId, volume); } \ No newline at end of file diff --git a/EarTrumpet/App.xaml b/EarTrumpet/App.xaml index 176b2f05b..b3f4c64ae 100644 --- a/EarTrumpet/App.xaml +++ b/EarTrumpet/App.xaml @@ -1,5 +1,5 @@  + Startup="Application_Startup" Exit="App_OnExit"> diff --git a/EarTrumpet/App.xaml.cs b/EarTrumpet/App.xaml.cs index 87b96246f..9c2ccfe9c 100644 --- a/EarTrumpet/App.xaml.cs +++ b/EarTrumpet/App.xaml.cs @@ -1,12 +1,38 @@ -using System.Windows; +using System.Globalization; +using System.Reflection; +using System.Threading; +using System.Windows; namespace EarTrumpet { - public partial class App : Application + public partial class App { + private Mutex _mMutex; + private void Application_Startup(object sender, StartupEventArgs e) { + var assembly = Assembly.GetExecutingAssembly(); + bool mutexCreated; + var mutexName = string.Format(CultureInfo.InvariantCulture, "Local\\{{{0}}}{{{1}}}", assembly.GetType().GUID, assembly.GetName().Name); + + _mMutex = new Mutex(true, mutexName, out mutexCreated); + + if (!mutexCreated) + { + _mMutex = null; + Current.Shutdown(); + return; + } + new MainWindow(); } + + private void App_OnExit(object sender, ExitEventArgs e) + { + if (_mMutex == null) return; + _mMutex.ReleaseMutex(); + _mMutex.Close(); + _mMutex = null; + } } } diff --git a/EarTrumpet/EarTrumpet.csproj b/EarTrumpet/EarTrumpet.csproj index d0d598a7f..f3ea7e4cc 100644 --- a/EarTrumpet/EarTrumpet.csproj +++ b/EarTrumpet/EarTrumpet.csproj @@ -58,11 +58,13 @@ MSBuild:Compile Designer + + diff --git a/EarTrumpet/EarTrumpet.iss b/EarTrumpet/EarTrumpet.iss index d769ac21b..3e63bf729 100644 --- a/EarTrumpet/EarTrumpet.iss +++ b/EarTrumpet/EarTrumpet.iss @@ -3,6 +3,7 @@ [Setup] AppName=Ear Trumpet AppVersion={#AppVer} +AppId=BA8684A3-9834-4D78-A666-04E88FF0EC82 VersionInfoVersion={#AppVer} DefaultDirName={pf}\Ear Trumpet DefaultGroupName=Ear Trumpet @@ -20,6 +21,8 @@ AllowUNCPath=no DisableReadyPage=yes DisableStartupPrompt=yes DisableWelcomePage=yes +MinVersion=10.0 +SetupMutex=EarTrumpetSetup [Files] Source: "EarTrumpet.exe"; DestDir: "{app}"; Flags: replacesameversion diff --git a/EarTrumpet/Extensions/BlurWindowExtensions.cs b/EarTrumpet/Extensions/BlurWindowExtensions.cs index 8abf8d996..130d960a4 100644 --- a/EarTrumpet/Extensions/BlurWindowExtensions.cs +++ b/EarTrumpet/Extensions/BlurWindowExtensions.cs @@ -1,7 +1,8 @@ -using System; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Interop; +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using EarTrumpet.Services; namespace EarTrumpet.Extensions { @@ -23,10 +24,22 @@ internal struct WindowCompositionAttribData [StructLayout(LayoutKind.Sequential)] internal struct AccentPolicy { - public AccentState AccentState; - public int AccentFlags; + public AccentState AccentState; + public AccentFlags AccentFlags; public int GradientColor; public int AnimationId; + } + + [Flags] + internal enum AccentFlags + { + // ... + DrawLeftBorder = 0x20, + DrawTopBorder = 0x40, + DrawRightBorder = 0x80, + DrawBottomBorder = 0x100, + DrawAllBorders = (DrawLeftBorder | DrawTopBorder | DrawRightBorder | DrawBottomBorder) + // ... } internal enum WindowCompositionAttribute @@ -66,7 +79,8 @@ private static void SetAccentPolicy(Window window, Interop.AccentState accentSta var windowHelper = new WindowInteropHelper(window); var accent = new Interop.AccentPolicy(); - accent.AccentState = accentState; + accent.AccentState = accentState; + accent.AccentFlags = GetAccentFlagsForTaskbarPosition(); var accentStructSize = Marshal.SizeOf(accent); @@ -82,5 +96,31 @@ private static void SetAccentPolicy(Window window, Interop.AccentState accentSta Marshal.FreeHGlobal(accentPtr); } + + private static Interop.AccentFlags GetAccentFlagsForTaskbarPosition() + { + var flags = Interop.AccentFlags.DrawAllBorders; + + switch(TaskbarService.TaskbarPosition) + { + case TaskbarPosition.Top: + flags &= ~Interop.AccentFlags.DrawTopBorder; + break; + + case TaskbarPosition.Bottom: + flags &= ~Interop.AccentFlags.DrawBottomBorder; + break; + + case TaskbarPosition.Left: + flags &= ~Interop.AccentFlags.DrawLeftBorder; + break; + + case TaskbarPosition.Right: + flags &= ~Interop.AccentFlags.DrawRightBorder; + break; + } + + return flags; + } } } diff --git a/EarTrumpet/Extensions/CollectionExtensions.cs b/EarTrumpet/Extensions/CollectionExtensions.cs index bca88b6cb..13e679e20 100644 --- a/EarTrumpet/Extensions/CollectionExtensions.cs +++ b/EarTrumpet/Extensions/CollectionExtensions.cs @@ -7,7 +7,7 @@ public static class CollectionExtensions { public static void AddRange(this ICollection destination, IEnumerable source) { - foreach (T item in source) + foreach (var item in source) { destination.Add(item); } @@ -15,7 +15,7 @@ public static void AddRange(this ICollection destination, IEnumerable s public static void AddSorted(this ObservableCollection collection, T item, IComparer comparer) { - int i = 0; + var i = 0; while ((i < collection.Count) && (comparer.Compare(collection[i], item) < 0)) { i++; diff --git a/EarTrumpet/Extensions/DoubleExtensions.cs b/EarTrumpet/Extensions/DoubleExtensions.cs new file mode 100644 index 000000000..4d679d3f4 --- /dev/null +++ b/EarTrumpet/Extensions/DoubleExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using EarTrumpet.Services; + +namespace EarTrumpet.Extensions +{ + static class DoubleExtensions + { + public static double Bound(this double val, double min, double max) + { + return Math.Max(min, Math.Min(max, val)); + } + } +} diff --git a/EarTrumpet/Extensions/IconExtensions.cs b/EarTrumpet/Extensions/IconExtensions.cs index f38520770..cb17f310a 100644 --- a/EarTrumpet/Extensions/IconExtensions.cs +++ b/EarTrumpet/Extensions/IconExtensions.cs @@ -22,8 +22,8 @@ public static class IconExtensions public static ImageSource ToImageSource(this Icon icon) { - Bitmap bitmap = icon.ToBitmap(); - IntPtr hBitmap = bitmap.GetHbitmap(); + var bitmap = icon.ToBitmap(); + var hBitmap = bitmap.GetHbitmap(); ImageSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( hBitmap, diff --git a/EarTrumpet/Extensions/SliderExtensions.cs b/EarTrumpet/Extensions/SliderExtensions.cs index d9bfa31db..4b95ef117 100644 --- a/EarTrumpet/Extensions/SliderExtensions.cs +++ b/EarTrumpet/Extensions/SliderExtensions.cs @@ -9,7 +9,12 @@ public static void SetPositionByControlPoint(this Slider slider, Point point) { var percent = point.X / slider.ActualWidth; var newValue = (slider.Maximum - slider.Minimum) * percent; - slider.Value = (newValue > slider.Maximum ? slider.Maximum : (newValue < slider.Minimum ? slider.Minimum : newValue)); + slider.Value = newValue.Bound(slider.Minimum,slider.Maximum); + } + + public static void ChangePositionByAmount(this Slider slider, double amount) + { + slider.Value = (slider.Value + amount).Bound(slider.Minimum, slider.Maximum); } } } diff --git a/EarTrumpet/Extensions/WindowExtensions.cs b/EarTrumpet/Extensions/WindowExtensions.cs index 7dfc191f0..e84df4bea 100644 --- a/EarTrumpet/Extensions/WindowExtensions.cs +++ b/EarTrumpet/Extensions/WindowExtensions.cs @@ -7,48 +7,90 @@ namespace EarTrumpet.Extensions { internal static class WindowExtensions { + private static bool _windowVisible; + + public static bool IsWindowVisible(this Window window) + { + return _windowVisible; + } + public static void HideWithAnimation(this Window window) { - TimeSpan slidetime = TimeSpan.FromSeconds(0.2); - DoubleAnimation topAnimation = new DoubleAnimation(); - topAnimation.Duration = new Duration(slidetime); - topAnimation.From = window.Top; - topAnimation.To = window.Top + 10; - topAnimation.FillBehavior = FillBehavior.Stop; - var easing = new QuinticEase(); - easing.EasingMode = EasingMode.EaseIn; - topAnimation.EasingFunction = easing; - topAnimation.Completed += (s, e) => + var hideAnimation = new DoubleAnimation + { + Duration = new Duration(TimeSpan.FromSeconds(0.2)), + FillBehavior = FillBehavior.Stop, + EasingFunction = new ExponentialEase {EasingMode = EasingMode.EaseIn} + }; + var taskbarPosition = TaskbarService.TaskbarPosition; + switch (taskbarPosition) + { + case TaskbarPosition.Left: + case TaskbarPosition.Right: + hideAnimation.From = window.Left; + break; + default: + hideAnimation.From = window.Top; + break; + } + hideAnimation.To = (taskbarPosition == TaskbarPosition.Top || taskbarPosition == TaskbarPosition.Left) ? hideAnimation.From - 10 : hideAnimation.From + 10; + hideAnimation.Completed += (s, e) => { window.Visibility = Visibility.Hidden; }; - window.BeginAnimation(Window.TopProperty, topAnimation); + + switch (taskbarPosition) + { + case TaskbarPosition.Left: + case TaskbarPosition.Right: + window.ApplyAnimationClock(Window.LeftProperty, hideAnimation.CreateClock()); + break; + default: + window.ApplyAnimationClock(Window.TopProperty, hideAnimation.CreateClock()); + break; + } + _windowVisible = false; } public static void ShowwithAnimation(this Window window) - { + { window.Visibility = Visibility.Visible; - window.Topmost = false; - TimeSpan slidetime = TimeSpan.FromSeconds(0.3); - DoubleAnimation bottomAnimation = new DoubleAnimation(); - bottomAnimation.Duration = new Duration(slidetime); - double top = window.Top; - bottomAnimation.From = window.Top + 25; - bottomAnimation.To = window.Top; - bottomAnimation.FillBehavior = FillBehavior.Stop; - bottomAnimation.Completed += (s, e) => + window.Topmost = false; + window.Activate(); + var showAnimation = new DoubleAnimation + { + Duration = new Duration(TimeSpan.FromSeconds(0.3)), + FillBehavior = FillBehavior.Stop, + EasingFunction = new ExponentialEase { EasingMode = EasingMode.EaseOut } + }; + var taskbarPosition = TaskbarService.TaskbarPosition; + switch (taskbarPosition) + { + case TaskbarPosition.Left: + case TaskbarPosition.Right: + showAnimation.To = window.Left; + break; + default: + showAnimation.To = window.Top; + break; + } + showAnimation.From = (taskbarPosition == TaskbarPosition.Top || taskbarPosition == TaskbarPosition.Left) ? showAnimation.To - 25 : showAnimation.To + 25; + showAnimation.Completed += (s, e) => { window.Topmost = true; - // Set the final position again. This covers a case where frames are dropped. - // and the window ends up over the taskbar instead. - window.Top = top; - window.Activate(); - window.Focus(); + window.Focus(); }; - var easing = new QuinticEase(); - easing.EasingMode = EasingMode.EaseOut; - bottomAnimation.EasingFunction = easing; - window.BeginAnimation(Window.TopProperty, bottomAnimation); + switch (taskbarPosition) + { + case TaskbarPosition.Left: + case TaskbarPosition.Right: + window.ApplyAnimationClock(Window.LeftProperty, showAnimation.CreateClock()); + break; + default: + window.ApplyAnimationClock(Window.TopProperty, showAnimation.CreateClock()); + break; + } + _windowVisible = true; } } } diff --git a/EarTrumpet/MainWindow.xaml b/EarTrumpet/MainWindow.xaml index 3e7919fd6..4eb360657 100644 --- a/EarTrumpet/MainWindow.xaml +++ b/EarTrumpet/MainWindow.xaml @@ -1,6 +1,7 @@  @@ -179,7 +180,7 @@ HorizontalContentAlignment="Stretch" Focusable="False"> - + @@ -189,14 +190,11 @@ - - - - > 24); // A colorBytes[1] = (byte)((0x00FF0000 & abgrValue) >> 16); // B - colorBytes[2] = (byte)((0x0000FF00 & abgrValue) >> 8); // G + colorBytes[2] = (byte)((0x0000FF00 & abgrValue) >> 8); // G colorBytes[3] = (byte)(0x000000FF & abgrValue); // R return Color.FromArgb(colorBytes[0], colorBytes[3], colorBytes[2], colorBytes[1]); } - } + } } diff --git a/EarTrumpet/Services/EarTrumpetAudioSessionService.cs b/EarTrumpet/Services/EarTrumpetAudioSessionService.cs index a5fbb77c8..c1d73a85d 100644 --- a/EarTrumpet/Services/EarTrumpetAudioSessionService.cs +++ b/EarTrumpet/Services/EarTrumpetAudioSessionService.cs @@ -6,43 +6,43 @@ namespace EarTrumpet.Services { - public class EarTrumpetAudioSessionService - { - static class Interop - { - [DllImport("EarTrumpet.Interop.dll")] - public static extern int RefreshAudioSessions(); + public class EarTrumpetAudioSessionService + { + static class Interop + { + [DllImport("EarTrumpet.Interop.dll")] + public static extern int RefreshAudioSessions(); - [DllImport("EarTrumpet.Interop.dll")] - public static extern int GetAudioSessionCount(); + [DllImport("EarTrumpet.Interop.dll")] + public static extern int GetAudioSessionCount(); - [DllImport("EarTrumpet.Interop.dll")] - public static extern int GetAudioSessions(ref IntPtr sessions); + [DllImport("EarTrumpet.Interop.dll")] + public static extern int GetAudioSessions(ref IntPtr sessions); - [DllImport("EarTrumpet.Interop.dll")] + [DllImport("EarTrumpet.Interop.dll")] public static extern int SetAudioSessionVolume(uint sessionId, float volume); - } - - public IEnumerable GetAudioSessions() - { - Interop.RefreshAudioSessions(); - - var sessionCount = Interop.GetAudioSessionCount(); - var sessions = new List(); - - IntPtr rawSessionsPtr = IntPtr.Zero; - Interop.GetAudioSessions(ref rawSessionsPtr); - - var sizeOfAudioSessionStruct = Marshal.SizeOf(typeof(EarTrumpetAudioSessionModel)); - for(int i = 0; i < sessionCount; i++) - { - var window = new IntPtr(rawSessionsPtr.ToInt64() + (sizeOfAudioSessionStruct * i)); - - var session = (EarTrumpetAudioSessionModel)Marshal.PtrToStructure(window, typeof(EarTrumpetAudioSessionModel)); - sessions.Add(session); - } - return sessions; - } + } + + public IEnumerable GetAudioSessions() + { + Interop.RefreshAudioSessions(); + + var sessionCount = Interop.GetAudioSessionCount(); + var sessions = new List(); + + var rawSessionsPtr = IntPtr.Zero; + Interop.GetAudioSessions(ref rawSessionsPtr); + + var sizeOfAudioSessionStruct = Marshal.SizeOf(typeof(EarTrumpetAudioSessionModel)); + for(var i = 0; i < sessionCount; i++) + { + var window = new IntPtr(rawSessionsPtr.ToInt64() + (sizeOfAudioSessionStruct * i)); + + var session = (EarTrumpetAudioSessionModel)Marshal.PtrToStructure(window, typeof(EarTrumpetAudioSessionModel)); + sessions.Add(session); + } + return sessions; + } public IEnumerable GetAudioSessionGroups() { @@ -53,7 +53,7 @@ public IEnumerable GetAudioSessionGroups() public void SetAudioSessionVolume(uint sessionId, float volume) { - Interop.SetAudioSessionVolume(sessionId, volume); + Interop.SetAudioSessionVolume(sessionId, volume); } - } + } } diff --git a/EarTrumpet/Services/TaskbarService.cs b/EarTrumpet/Services/TaskbarService.cs new file mode 100644 index 000000000..3324e6720 --- /dev/null +++ b/EarTrumpet/Services/TaskbarService.cs @@ -0,0 +1,83 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Forms; + +namespace EarTrumpet.Services +{ + public sealed class TaskbarService + { + private const string ClassName = "Shell_TrayWnd"; + + public static Rectangle TaskbarPostionRect + { + get + { + var taskbarHandle = User32.FindWindow(ClassName, null); + + var r = new RECT(); + User32.GetWindowRect(taskbarHandle, ref r); + + return Rectangle.FromLTRB(r.left, r.top, r.right, r.bottom); + } + } + + public static TaskbarPosition TaskbarPosition + { + get + { + var rect = TaskbarPostionRect; + var screen = TaskbarScreen; + if (screen == null) return TaskbarPosition.Bottom; + + if (rect.Bottom == screen.Bounds.Bottom && rect.Top == screen.Bounds.Top) + { + return (rect.Left == screen.Bounds.Left) ? TaskbarPosition.Left : TaskbarPosition.Right; + } + if (rect.Right == screen.Bounds.Right && rect.Left == screen.Bounds.Left) + { + return (rect.Top == screen.Bounds.Top) ? TaskbarPosition.Top : TaskbarPosition.Bottom; + } + return TaskbarPosition.Bottom; + } + } + + public static Screen TaskbarScreen + { + get + { + var rect = TaskbarPostionRect; + return Screen.AllScreens.FirstOrDefault(x => x.Bounds.Contains(rect)); + } + } + } + + public static class User32 + { + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + public enum TaskbarPosition + { + Top, + Left, + Right, + Bottom + } +} \ No newline at end of file diff --git a/EarTrumpet/Services/ThemeService.cs b/EarTrumpet/Services/ThemeService.cs index 65fad1b30..0c8f4396e 100644 --- a/EarTrumpet/Services/ThemeService.cs +++ b/EarTrumpet/Services/ThemeService.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Media; namespace EarTrumpet.Services @@ -8,10 +7,7 @@ public class ThemeService { public static bool IsWindowTransparencyEnabled { - get - { - return !SystemParameters.HighContrast && UserSystemPreferencesService.IsTransparencyEnabled; - } + get { return !SystemParameters.HighContrast && UserSystemPreferencesService.IsTransparencyEnabled; } } public static void UpdateThemeResources(ResourceDictionary dictionary) @@ -37,23 +33,23 @@ private static Color GetWindowBackgroundColor() } else if (UserSystemPreferencesService.UseAccentColor) { - resource = ThemeService.IsWindowTransparencyEnabled ? "ImmersiveSystemAccentDark2" : "ImmersiveSystemAccentDark1"; + resource = IsWindowTransparencyEnabled ? "ImmersiveSystemAccentDark2" : "ImmersiveSystemAccentDark1"; } else { resource = "ImmersiveDarkChromeMedium"; } - Color color = AccentColorService.GetColorByTypeName(resource); - color.A = (byte)(ThemeService.IsWindowTransparencyEnabled ? 190 : 255); + var color = AccentColorService.GetColorByTypeName(resource); + color.A = (byte) (IsWindowTransparencyEnabled ? 190 : 255); return color; } private static void SetBrushWithOpacity(ResourceDictionary dictionary, string name, string immersiveAccentName, double opacity) { - Color color = AccentColorService.GetColorByTypeName(immersiveAccentName); - color.A = (byte)(opacity * 255); - ((SolidColorBrush)dictionary[name]).Color = color; + var color = AccentColorService.GetColorByTypeName(immersiveAccentName); + color.A = (byte) (opacity*255); + ((SolidColorBrush) dictionary[name]).Color = color; } private static void SetBrush(ResourceDictionary dictionary, string name, string immersiveAccentName) @@ -66,4 +62,4 @@ private static void ReplaceBrush(ResourceDictionary dictionary, string name, str dictionary[name] = new SolidColorBrush(AccentColorService.GetColorByTypeName(immersiveAccentName)); } } -} +} \ No newline at end of file diff --git a/EarTrumpet/Services/UserPreferencesService.cs b/EarTrumpet/Services/UserPreferencesService.cs index 9f6445171..0c1baed90 100644 --- a/EarTrumpet/Services/UserPreferencesService.cs +++ b/EarTrumpet/Services/UserPreferencesService.cs @@ -1,6 +1,6 @@ -using Microsoft.Win32; -using System; +using System; using System.Diagnostics; +using Microsoft.Win32; namespace EarTrumpet.Services { @@ -12,7 +12,7 @@ public static bool ShowDesktopApps { try { - return 1 == (int)Registry.CurrentUser.CreateSubKey(@"Software\EarTrumpet").GetValue("ShowDesktopApps"); + return 1 == (int) Registry.CurrentUser.CreateSubKey(@"Software\EarTrumpet").GetValue("ShowDesktopApps"); } catch (Exception e) { @@ -27,4 +27,4 @@ public static bool ShowDesktopApps } } } -} +} \ No newline at end of file diff --git a/EarTrumpet/Services/UserSystemPreferencesService.cs b/EarTrumpet/Services/UserSystemPreferencesService.cs index bf969f2f4..7a3117620 100644 --- a/EarTrumpet/Services/UserSystemPreferencesService.cs +++ b/EarTrumpet/Services/UserSystemPreferencesService.cs @@ -2,28 +2,28 @@ namespace EarTrumpet.Services { - public static class UserSystemPreferencesService - { - public static bool IsTransparencyEnabled - { - get - { - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) - { - return (int)baseKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").GetValue("EnableTransparency", 0) > 0; - } - } - } + public static class UserSystemPreferencesService + { + public static bool IsTransparencyEnabled + { + get + { + using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) + { + return (int)baseKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").GetValue("EnableTransparency", 0) > 0; + } + } + } - public static bool UseAccentColor - { - get - { - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) - { - return (int)baseKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").GetValue("ColorPrevalence", 0) > 0; - } - } - } + public static bool UseAccentColor + { + get + { + using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) + { + return (int)baseKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").GetValue("ColorPrevalence", 0) > 0; + } + } + } } } diff --git a/EarTrumpet/ViewModels/AppItemViewModel.cs b/EarTrumpet/ViewModels/AppItemViewModel.cs index 270332c7d..eaf251485 100644 --- a/EarTrumpet/ViewModels/AppItemViewModel.cs +++ b/EarTrumpet/ViewModels/AppItemViewModel.cs @@ -21,7 +21,7 @@ public class AppItemViewModel : BindableBase public double IconHeight { get; set; } public double IconWidth { get; set; } - private int _volume = 0; + private int _volume; public int Volume { get @@ -36,13 +36,13 @@ public int Volume foreach (var session in _sessions.Sessions) { - _callback.SetVolume(session, (float)_volume / 100.0f); + _callback.SetVolume(session, _volume / 100.0f); } RaisePropertyChanged("Volume"); } } } - public Color Background { get; set; } + public SolidColorBrush Background { get; set; } public bool IsDesktop { get; set; } public AppItemViewModel(IAudioMixerViewModelCallback callback, EarTrumpetAudioSessionModelGroup sessions) @@ -64,18 +64,14 @@ public AppItemViewModel(IAudioMixerViewModelCallback callback, EarTrumpetAudioSe { try { - if (Path.GetExtension(session.IconPath) == ".dll") - { - Icon = IconExtensions.ExtractIconFromDll(session.IconPath); - } - else - { - Icon = System.Drawing.Icon.ExtractAssociatedIcon(session.IconPath).ToImageSource(); - } + Icon = Path.GetExtension(session.IconPath) == ".dll" ? IconExtensions.ExtractIconFromDll(session.IconPath) : System.Drawing.Icon.ExtractAssociatedIcon(session.IconPath).ToImageSource(); + } + catch + { + // ignored } - catch { } - Background = Colors.Transparent; + Background = new SolidColorBrush(Colors.Transparent); try { @@ -89,31 +85,25 @@ public AppItemViewModel(IAudioMixerViewModelCallback callback, EarTrumpetAudioSe } else { - Icon = new BitmapImage(new Uri(session.IconPath)); - Background = AccentColorService.FromABGR(session.BackgroundColor); + if (File.Exists(session.IconPath)) //hack until we invoke the resource manager correctly. + { + Icon = new BitmapImage(new Uri(session.IconPath)); + } + Background = new SolidColorBrush(AccentColorService.FromABGR(session.BackgroundColor)); } } public void UpdateFromOther(AppItemViewModel other) { - if (_volume != other.Volume) - { - _sessions = other._sessions; - _volume = other.Volume; - RaisePropertyChanged("Volume"); - } + if (_volume == other.Volume) return; + _sessions = other._sessions; + _volume = other.Volume; + RaisePropertyChanged("Volume"); } public bool IsSame(AppItemViewModel other) { - foreach (var session in other._sessions.Sessions) - { - if (_sessions.Sessions.Where(x => x.SessionId == session.SessionId).Any()) - { - return true; - } - } - return false; + return other._sessions.Sessions.Any(session => _sessions.Sessions.Any(x => x.SessionId == session.SessionId)); } } } diff --git a/EarTrumpet/ViewModels/AppItemViewModelComparer.cs b/EarTrumpet/ViewModels/AppItemViewModelComparer.cs index 2a910024a..61150e42d 100644 --- a/EarTrumpet/ViewModels/AppItemViewModelComparer.cs +++ b/EarTrumpet/ViewModels/AppItemViewModelComparer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace EarTrumpet.ViewModels { @@ -8,7 +9,7 @@ public class AppItemViewModelComparer : IComparer public int Compare(AppItemViewModel one, AppItemViewModel two) { - return one.DisplayName.CompareTo(two.DisplayName); + return string.Compare(one.DisplayName, two.DisplayName, StringComparison.Ordinal); } } } diff --git a/EarTrumpet/ViewModels/AudioMixerViewModel.cs b/EarTrumpet/ViewModels/AudioMixerViewModel.cs index 07d1dfd71..85bd368ce 100644 --- a/EarTrumpet/ViewModels/AudioMixerViewModel.cs +++ b/EarTrumpet/ViewModels/AudioMixerViewModel.cs @@ -63,7 +63,7 @@ public void Refresh() // add new apps foreach (var session in sessions) { - var findApp = Apps.Where(x => x.IsSame(session)).FirstOrDefault(); + var findApp = Apps.FirstOrDefault(x => x.IsSame(session)); if (findApp == null) { if (!session.IsDesktop || UserPreferencesService.ShowDesktopApps) diff --git a/EarTrumpet/ViewModels/IAudioMixerViewModelCallback.cs b/EarTrumpet/ViewModels/IAudioMixerViewModelCallback.cs index 203982cc1..43eaac167 100644 --- a/EarTrumpet/ViewModels/IAudioMixerViewModelCallback.cs +++ b/EarTrumpet/ViewModels/IAudioMixerViewModelCallback.cs @@ -1,5 +1,4 @@ - -using EarTrumpet.Models; +using EarTrumpet.Models; namespace EarTrumpet.ViewModels { diff --git a/README.md b/README.md index ae5654b47..6eb3c5380 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # EarTrumpet -Icon created by Artjom Kormanfrom the Noun Project +![Ear Trumpet Screenshot](https://pbs.twimg.com/media/CJoDCsjUMAAeWq7.png:large) + +## Supported operating systems ## +- Windows 10 + +## Credits ## +- David Golden (@GoldenTao) +- Rafael Rivera (@RiveraR) + +*Ear Trumpet* icon created by Artjom Kormanfrom from the Noun Project