Repository: NickeManarin/ScreenToGif Branch: master Commit: db18fb5babb8 Files: 804 Total size: 9.6 MB Directory structure: gitextract_e0qxbpyj/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── feature-request.md │ └── workflows/ │ └── discord-releases.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Packages.props ├── GifRecorder.sln ├── LICENSE.txt ├── LOCALIZATION.md ├── Other/ │ └── Translator/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── Controls/ │ │ ├── ExtendedTextBox.cs │ │ ├── ImageButton.cs │ │ ├── ImageMenuItem.cs │ │ └── StatusBand.cs │ ├── Converters/ │ │ ├── MultiLineTitle.cs │ │ └── NullToInvertedBool.cs │ ├── Dialog.xaml │ ├── Dialog.xaml.cs │ ├── ExceptionDialog.xaml │ ├── ExceptionDialog.xaml.cs │ ├── ExceptionViewer.xaml │ ├── ExceptionViewer.xaml.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Themes/ │ │ ├── Buttons.xaml │ │ ├── Colors.xaml │ │ ├── ComboBox.xaml │ │ ├── DataGridStyle.xaml │ │ ├── Generic.xaml │ │ ├── IconSet.xaml │ │ └── ProgressBar.xaml │ ├── Translator.csproj │ ├── TranslatorWindow.xaml │ ├── TranslatorWindow.xaml.cs │ └── Util/ │ ├── DataGridHelper.cs │ ├── LogWriter.cs │ └── VisualHelper.cs ├── README.md ├── ScreenToGif/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── Capture/ │ │ ├── BaseCapture.cs │ │ ├── CachedCapture.cs │ │ ├── DirectCachedCapture.cs │ │ ├── DirectChangedCachedCapture.cs │ │ ├── DirectChangedImageCapture.cs │ │ ├── DirectImageCapture.cs │ │ ├── ICapture.cs │ │ ├── ImageCapture.cs │ │ └── RegionSelectHelper.cs │ ├── Cloud/ │ │ ├── CloudFactory.cs │ │ ├── Imgur.cs │ │ └── YandexDisk.cs │ ├── Controls/ │ │ ├── AdornerControl.cs │ │ ├── AttachmentListBoxItem.cs │ │ ├── AwareTabItem.cs │ │ ├── BaseRecorder.cs │ │ ├── BaseScreenRecorder.cs │ │ ├── BaseWindow.cs │ │ ├── Card.cs │ │ ├── CircularProgressBar.cs │ │ ├── ColorBox.cs │ │ ├── CroppingAdorner.cs │ │ ├── DataGridHeaderBorder.cs │ │ ├── DecimalBox.cs │ │ ├── DecimalUpDown.cs │ │ ├── DisplayTimer.cs │ │ ├── DoubleBox.cs │ │ ├── DoubleUpDown.cs │ │ ├── DragScrollGrid.cs │ │ ├── DrawingCanvas.cs │ │ ├── DropDownButton.cs │ │ ├── DynamicGrid.cs │ │ ├── ElementAdorner.cs │ │ ├── EncoderListViewItem.cs │ │ ├── ExListViewItem.cs │ │ ├── ExWindow.cs │ │ ├── ExtendedButton.cs │ │ ├── ExtendedCheckBox.cs │ │ ├── ExtendedComboBox.cs │ │ ├── ExtendedListBoxItem.cs │ │ ├── ExtendedMenuItem.cs │ │ ├── ExtendedProgressBar.cs │ │ ├── ExtendedRadioButton.cs │ │ ├── ExtendedRepeatButton.cs │ │ ├── ExtendedSlider.cs │ │ ├── ExtendedTextBox.cs │ │ ├── ExtendedToggleButton.cs │ │ ├── ExtendedUniformGrid.cs │ │ ├── FolderSelector.cs │ │ ├── FrameViewer.cs │ │ ├── FrameworkElementAdorner.cs │ │ ├── HeaderedTooltip.cs │ │ ├── HexadecimalBox.cs │ │ ├── HideableTabControl.cs │ │ ├── InkCanvasExtended.cs │ │ ├── IntegerBox.cs │ │ ├── IntegerUpDown.cs │ │ ├── Items/ │ │ │ ├── EncoderItem.cs │ │ │ ├── ExportItem.cs │ │ │ ├── GenericItem.cs │ │ │ └── QuantizationMethodItem.cs │ │ ├── KeyBox.cs │ │ ├── LabelSeparator.cs │ │ ├── LightWindow.cs │ │ ├── MoveResizeControl.cs │ │ ├── NotificationBox.cs │ │ ├── NotifyIcon.cs │ │ ├── NullableIntegerBox.cs │ │ ├── NullableIntegerUpDown.cs │ │ ├── PuncturedRect.cs │ │ ├── RadialPanel.cs │ │ ├── RangeSlider.cs │ │ ├── ResizingAdorner.cs │ │ ├── SelectControl.cs │ │ ├── SelectControlOld.cs │ │ ├── Shapes/ │ │ │ ├── Arrow.cs │ │ │ └── Triangle.cs │ │ ├── SpectrumSlider.cs │ │ ├── SplitButton.cs │ │ ├── StatusBand.cs │ │ ├── StatusList.cs │ │ ├── TextPath.cs │ │ ├── TimeBox.cs │ │ ├── WebcamControl.xaml │ │ ├── WebcamControl.xaml.cs │ │ └── ZoomBox.cs │ ├── Docs/ │ │ └── Documentation.md │ ├── ImageUtil/ │ │ └── ImageMethods.cs │ ├── Model/ │ │ ├── FrameInfo.cs │ │ └── ProjectInfo.cs │ ├── Properties/ │ │ ├── AssemblyInfo.cs │ │ └── PublishProfiles/ │ │ ├── Publish as ARM64 (light).pubxml │ │ ├── Publish as ARM64 (self-contained).pubxml │ │ ├── Publish as ARM64.pubxml │ │ ├── Publish as x64 (light).pubxml │ │ ├── Publish as x64 (self-contained).pubxml │ │ ├── Publish as x64.pubxml │ │ ├── Publish as x86 (light).pubxml │ │ ├── Publish as x86 (self-contained).pubxml │ │ └── Publish as x86.pubxml │ ├── Readme.md │ ├── Resources/ │ │ ├── Backgrounds.xaml │ │ ├── Commands.xaml │ │ ├── Flags.xaml │ │ ├── Glyphs.xaml │ │ ├── Localization/ │ │ │ ├── StringResources.ar.xaml │ │ │ ├── StringResources.cs.xaml │ │ │ ├── StringResources.da.xaml │ │ │ ├── StringResources.de.xaml │ │ │ ├── StringResources.el.xaml │ │ │ ├── StringResources.en.xaml │ │ │ ├── StringResources.es-AR.xaml │ │ │ ├── StringResources.es.xaml │ │ │ ├── StringResources.fi.xaml │ │ │ ├── StringResources.fr.xaml │ │ │ ├── StringResources.he.xaml │ │ │ ├── StringResources.hu.xaml │ │ │ ├── StringResources.it.xaml │ │ │ ├── StringResources.ja.xaml │ │ │ ├── StringResources.ko.xaml │ │ │ ├── StringResources.nl.xaml │ │ │ ├── StringResources.pl.xaml │ │ │ ├── StringResources.pt-PT.xaml │ │ │ ├── StringResources.pt.xaml │ │ │ ├── StringResources.ru.xaml │ │ │ ├── StringResources.sv.xaml │ │ │ ├── StringResources.sw.xaml │ │ │ ├── StringResources.ta.xaml │ │ │ ├── StringResources.tr.xaml │ │ │ ├── StringResources.uk.xaml │ │ │ ├── StringResources.vi.xaml │ │ │ ├── StringResources.zh-Hant.xaml │ │ │ └── StringResources.zh.xaml │ │ ├── Settings.xaml │ │ ├── Style.css │ │ └── Vectors.xaml │ ├── ScreenToGif.csproj │ ├── Themes/ │ │ ├── Button.xaml │ │ ├── Colors/ │ │ │ ├── Dark.xaml │ │ │ ├── Light.xaml │ │ │ ├── Medium.xaml │ │ │ └── VeryDark.xaml │ │ ├── ComboBox.xaml │ │ ├── Common.xaml │ │ ├── Controls/ │ │ │ └── ExtendedComboBox.xaml │ │ ├── DataGrid.xaml │ │ ├── EncoderListViewItem.xaml │ │ ├── Generic.xaml │ │ └── Old.xaml │ ├── UserControls/ │ │ ├── BorderPanel.xaml │ │ ├── BorderPanel.xaml.cs │ │ ├── DelayPanel.xaml │ │ ├── DelayPanel.xaml.cs │ │ ├── ExportPanel.xaml │ │ ├── ExportPanel.xaml.cs │ │ ├── ImageViewer.xaml │ │ ├── ImageViewer.xaml.cs │ │ ├── ImgurPanel.xaml │ │ ├── ImgurPanel.xaml.cs │ │ ├── KGySoftGifOptionsPanel.xaml │ │ ├── KGySoftGifOptionsPanel.xaml.cs │ │ ├── KeyStrokesPanel.xaml │ │ ├── KeyStrokesPanel.xaml.cs │ │ ├── MouseClicksPanel.xaml │ │ ├── MouseClicksPanel.xaml.cs │ │ ├── ProgressPanel.xaml │ │ ├── ProgressPanel.xaml.cs │ │ ├── ResizePanel.xaml │ │ ├── ResizePanel.xaml.cs │ │ ├── ShadowPanel.xaml │ │ ├── ShadowPanel.xaml.cs │ │ ├── YandexPanel.xaml │ │ └── YandexPanel.xaml.cs │ ├── Util/ │ │ ├── ActionStack.cs │ │ ├── BrushAnimation.cs │ │ ├── ClipBoard.cs │ │ ├── ColorExtensions.cs │ │ ├── ComboBoxItemTemplateSelector.cs │ │ ├── Commands.cs │ │ ├── Converters/ │ │ │ ├── CommandToKeyGesture.cs │ │ │ ├── KeyGestureToString.cs │ │ │ ├── PresetToSubViewModelConverter.cs │ │ │ ├── RoutedCommandToInputGestureText.cs │ │ │ └── ShortcutKeys.cs │ │ ├── EncodingManager.cs │ │ ├── Extensions/ │ │ │ ├── PresetExtensions.cs │ │ │ └── SettingsExtension.cs │ │ ├── FeedbackHelper.cs │ │ ├── FrameworkHelper.cs │ │ ├── Global.cs │ │ ├── NotificationManager.cs │ │ ├── Other.cs │ │ ├── ScreenHelper.cs │ │ ├── ScrollSynchronizer.cs │ │ ├── StorageUtils.cs │ │ └── ThemeHelper.cs │ ├── ViewModel/ │ │ ├── ApplicationBaseViewModel.cs │ │ ├── ApplicationViewModel.cs │ │ ├── DithererDescriptor.cs │ │ ├── KGySoftGifOptionsViewModel.cs │ │ └── QuantizerDescriptor.cs │ ├── Views/ │ │ └── Settings/ │ │ ├── AboutSettings.xaml │ │ ├── AboutSettings.xaml.cs │ │ ├── ApplicationSettings.xaml │ │ ├── ApplicationSettings.xaml.cs │ │ ├── DonateSettings.xaml │ │ ├── DonateSettings.xaml.cs │ │ ├── EditorSettings.xaml │ │ ├── EditorSettings.xaml.cs │ │ ├── LanguageSettings.xaml │ │ ├── LanguageSettings.xaml.cs │ │ ├── PluginSettings.xaml │ │ ├── PluginSettings.xaml.cs │ │ ├── RecorderSettings.xaml │ │ ├── RecorderSettings.xaml.cs │ │ ├── ShortcutsSettings.xaml │ │ ├── ShortcutsSettings.xaml.cs │ │ ├── StorageSettings.xaml │ │ ├── StorageSettings.xaml.cs │ │ ├── TasksSettings.xaml │ │ ├── TasksSettings.xaml.cs │ │ ├── UploadSettings.xaml │ │ └── UploadSettings.xaml.cs │ ├── Webcam/ │ │ ├── DirectShow/ │ │ │ ├── ControlStreaming.cs │ │ │ ├── CoreStreaming.cs │ │ │ ├── Devices.cs │ │ │ ├── EditStreaming.cs │ │ │ ├── ExtendStreaming.cs │ │ │ ├── Marshaller.cs │ │ │ ├── Util.cs │ │ │ ├── Uuid.cs │ │ │ └── WorkAround.cs │ │ └── DirectX/ │ │ ├── CaptureWebcam.cs │ │ ├── Filter.cs │ │ ├── FilterCollection.cs │ │ └── Filters.cs │ ├── Windows/ │ │ ├── Board.xaml │ │ ├── Board.xaml.cs │ │ ├── Editor.xaml │ │ ├── Editor.xaml.cs │ │ ├── NewRecorder.xaml │ │ ├── NewRecorder.xaml.cs │ │ ├── Options.xaml │ │ ├── Options.xaml.cs │ │ ├── Other/ │ │ │ ├── AutomatedTask.xaml │ │ │ ├── AutomatedTask.xaml.cs │ │ │ ├── CacheDialog.xaml │ │ │ ├── CacheDialog.xaml.cs │ │ │ ├── ColorSelector.xaml │ │ │ ├── ColorSelector.xaml.cs │ │ │ ├── CommandPreviewer.xaml │ │ │ ├── CommandPreviewer.xaml.cs │ │ │ ├── Dialog.xaml │ │ │ ├── Dialog.xaml.cs │ │ │ ├── DownloadDialog.xaml │ │ │ ├── DownloadDialog.xaml.cs │ │ │ ├── Downloader.xaml │ │ │ ├── Downloader.xaml.cs │ │ │ ├── Encoder.xaml │ │ │ ├── Encoder.xaml.cs │ │ │ ├── ErrorDialog.xaml │ │ │ ├── ErrorDialog.xaml.cs │ │ │ ├── ExceptionDialog.xaml │ │ │ ├── ExceptionDialog.xaml.cs │ │ │ ├── ExceptionViewer.xaml │ │ │ ├── ExceptionViewer.xaml.cs │ │ │ ├── Feedback.xaml │ │ │ ├── Feedback.xaml.cs │ │ │ ├── FeedbackPreview.xaml │ │ │ ├── FeedbackPreview.xaml.cs │ │ │ ├── GoTo.xaml │ │ │ ├── GoTo.xaml.cs │ │ │ ├── GraphicsConfigurationDialog.xaml │ │ │ ├── GraphicsConfigurationDialog.xaml.cs │ │ │ ├── Insert.xaml │ │ │ ├── Insert.xaml.cs │ │ │ ├── KeyStrokes.xaml │ │ │ ├── KeyStrokes.xaml.cs │ │ │ ├── Localization.xaml │ │ │ ├── Localization.xaml.cs │ │ │ ├── PickAlbumDialog.xaml │ │ │ ├── PickAlbumDialog.xaml.cs │ │ │ ├── Preset.xaml │ │ │ ├── Preset.xaml.cs │ │ │ ├── RegionMagnifier.xaml │ │ │ ├── RegionMagnifier.xaml.cs │ │ │ ├── RegionSelection.xaml │ │ │ ├── RegionSelection.xaml.cs │ │ │ ├── RegionSelector.xaml │ │ │ ├── RegionSelector.xaml.cs │ │ │ ├── Splash.xaml │ │ │ ├── Splash.xaml.cs │ │ │ ├── Startup.xaml │ │ │ ├── Startup.xaml.cs │ │ │ ├── TestField.xaml │ │ │ ├── TestField.xaml.cs │ │ │ ├── TextDialog.xaml │ │ │ ├── TextDialog.xaml.cs │ │ │ ├── Troubleshoot.xaml │ │ │ ├── Troubleshoot.xaml.cs │ │ │ ├── Upload.xaml │ │ │ ├── Upload.xaml.cs │ │ │ ├── UploadHistory.xaml │ │ │ ├── UploadHistory.xaml.cs │ │ │ ├── VideoSource.xaml │ │ │ └── VideoSource.xaml.cs │ │ ├── Recorder.xaml │ │ ├── Recorder.xaml.cs │ │ ├── Webcam.xaml │ │ └── Webcam.xaml.cs │ └── app.manifest ├── ScreenToGif.Model/ │ ├── Enums/ │ │ ├── AdornerPlacement.cs │ │ ├── AppThemes.cs │ │ ├── ApplicationTypes.cs │ │ ├── CaptureFrequencies.cs │ │ ├── ColorQuantizationType.cs │ │ ├── CopyModes.cs │ │ ├── DelayChangeType.cs │ │ ├── DelayUpdateModes.cs │ │ ├── DitherMethods.cs │ │ ├── DrawingModeType.cs │ │ ├── DuplicatesDelayModes.cs │ │ ├── DuplicatesRemovalModes.cs │ │ ├── EncoderTypes.cs │ │ ├── EncodingStatus.cs │ │ ├── ExitAction.cs │ │ ├── ExportFormats.cs │ │ ├── ExtrasStatus.cs │ │ ├── FadeModes.cs │ │ ├── FlipRotateType.cs │ │ ├── Framerates.cs │ │ ├── GifskiErrorCodes.cs │ │ ├── HardwareAcceleration.cs │ │ ├── Icons.cs │ │ ├── ModeType.cs │ │ ├── MouseButtons.cs │ │ ├── MouseEventType.cs │ │ ├── Native/ │ │ │ ├── BaloonFlags.cs │ │ │ ├── BitmapCompressionModes.cs │ │ │ ├── CopyPixelOperations.cs │ │ │ ├── CornerPreferences.cs │ │ │ ├── DeviceCaps.cs │ │ │ ├── DibColorModes.cs │ │ │ ├── DisplayDeviceStates.cs │ │ │ ├── DisplayDevices.cs │ │ │ ├── DpiTypes.cs │ │ │ ├── DwmWindowAttributes.cs │ │ │ ├── GetAncestorFlags.cs │ │ │ ├── GetWindowTypes.cs │ │ │ ├── HitTestTargets.cs │ │ │ ├── IconDataMembers.cs │ │ │ ├── IconStates.cs │ │ │ ├── LocalMemoryFlags.cs │ │ │ ├── MapTypes.cs │ │ │ ├── MenuFunctions.cs │ │ │ ├── NativeMouseEvents.cs │ │ │ ├── NotifyCommands.cs │ │ │ ├── NotifyIconVersions.cs │ │ │ ├── ProcessDpiAwareness.cs │ │ │ ├── SetWindowPosFlags.cs │ │ │ ├── ShellExecuteMasks.cs │ │ │ ├── ShowWindowCommands.cs │ │ │ ├── SpecialWindowHandles.cs │ │ │ ├── SysCommands.cs │ │ │ ├── TimeResults.cs │ │ │ ├── WindowAttributes.cs │ │ │ ├── WindowStyles.cs │ │ │ ├── WindowStylesEx.cs │ │ │ └── WindowsMessages.cs │ │ ├── ObfuscationModes.cs │ │ ├── OverwriteModes.cs │ │ ├── PanelType.cs │ │ ├── PartialExportModes.cs │ │ ├── PasteBehaviors.cs │ │ ├── PredictionMethods.cs │ │ ├── ProgressTypes.cs │ │ ├── ProjectByType.cs │ │ ├── ProxyTypes.cs │ │ ├── RateUnits.cs │ │ ├── RecorderStages.cs │ │ ├── ReduceDelayModes.cs │ │ ├── ResizeDirection.cs │ │ ├── ScalingMethod.cs │ │ ├── SizeUnits.cs │ │ ├── SlideFromType.cs │ │ ├── SmoothLoopSelectionModes.cs │ │ ├── StatusReason.cs │ │ ├── StatusType.cs │ │ ├── SupportedFFmpegVersions.cs │ │ ├── TaskTypes.cs │ │ ├── UploadDestinations.cs │ │ ├── UploadService.cs │ │ ├── VideoCodecPresets.cs │ │ ├── VideoCodecs.cs │ │ ├── VideoPixelFormats.cs │ │ ├── VideoSettingsModes.cs │ │ └── Vsyncs.cs │ ├── Events/ │ │ ├── CustomKeyEventArgs.cs │ │ ├── CustomKeyPressEventArgs.cs │ │ ├── ManipulatedEventArgs.cs │ │ ├── SaveEventArgs.cs │ │ └── ValidatedEventArgs.cs │ ├── Exceptions/ │ │ ├── GraphicsConfigurationException.cs │ │ ├── SettingsPersistenceException.cs │ │ └── UploadException.cs │ ├── Interfaces/ │ │ ├── IExportPreset.cs │ │ ├── IFfmpegPreset.cs │ │ ├── IFrame.cs │ │ ├── IHistory.cs │ │ ├── IKeyGesture.cs │ │ ├── IPanel.cs │ │ ├── IPersistent.cs │ │ ├── IPreset.cs │ │ ├── IUploadPreset.cs │ │ └── IUploader.cs │ ├── Models/ │ │ ├── DetectedRegion.cs │ │ ├── ExportFrame.cs │ │ ├── ExportProject.cs │ │ ├── FosshubItem.cs │ │ ├── FosshubRelease.cs │ │ ├── FosshubResponse.cs │ │ ├── Frame.cs │ │ ├── GitHub/ │ │ │ ├── GitHubAsset.cs │ │ │ ├── GitHubRelease.cs │ │ │ └── GitHubUser.cs │ │ ├── MediaSource.cs │ │ ├── Native/ │ │ │ ├── Monitor.cs │ │ │ └── NativeRect.cs │ │ ├── Project/ │ │ │ ├── Project.cs │ │ │ ├── Sequence.cs │ │ │ ├── Sequences/ │ │ │ │ ├── BrushSequence.cs │ │ │ │ ├── CursorSequence.cs │ │ │ │ ├── DrawingSequence.cs │ │ │ │ ├── KeySequence.cs │ │ │ │ ├── ObfuscationSequence.cs │ │ │ │ ├── ProgressSequence.cs │ │ │ │ ├── RasterSequence.cs │ │ │ │ ├── ShapeSequence.cs │ │ │ │ ├── SizeableSequence.cs │ │ │ │ ├── SubSequences/ │ │ │ │ │ ├── CursorEvent.cs │ │ │ │ │ ├── Frame.cs │ │ │ │ │ ├── KeyEvent.cs │ │ │ │ │ └── Shadow.cs │ │ │ │ └── TextSequence.cs │ │ │ └── Track.cs │ │ ├── Property.cs │ │ ├── SimpleMouseGesture.cs │ │ ├── Upload/ │ │ │ ├── Imgur/ │ │ │ │ ├── ImgurAlbum.cs │ │ │ │ ├── ImgurAlbumsResponse.cs │ │ │ │ ├── ImgurImage.cs │ │ │ │ └── ImgurUploadResponse.cs │ │ │ ├── OAuth2Token.cs │ │ │ └── YandexDisk/ │ │ │ ├── Error.cs │ │ │ └── Link.cs │ │ └── VideoSource.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── ScreenToGif.Domain.csproj │ ├── Structs/ │ │ └── GifskiSettings.cs │ └── ViewModels/ │ ├── BaseViewModel.cs │ └── BindableBase.cs ├── ScreenToGif.Native/ │ ├── Constants.cs │ ├── Delegates.cs │ ├── External/ │ │ ├── DwmApi.cs │ │ ├── Gdi32.cs │ │ ├── Kernel32.cs │ │ ├── MsvCrt.cs │ │ ├── NtDll.cs │ │ ├── ShCore.cs │ │ ├── Shell32.cs │ │ ├── User32.cs │ │ └── WinMm.cs │ ├── Helpers/ │ │ ├── DllSecurity.cs │ │ ├── FunctionLoader.cs │ │ ├── HotKey.cs │ │ ├── Other.cs │ │ ├── TimerResolution.cs │ │ └── WindowMessageSink.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── ScreenToGif.Native.csproj │ └── Structs/ │ ├── Bitmap.cs │ ├── BitmapFileHeader.cs │ ├── BitmapInfoHeader.cs │ ├── CursorInfo.cs │ ├── IconInfo.cs │ ├── KeyboardHook.cs │ ├── Margins.cs │ ├── MemoryStatusEx.cs │ ├── MinMaxInfo.cs │ ├── MonitorInfoEx.cs │ ├── MouseHook.cs │ ├── NotifyIconData.cs │ ├── PointW.cs │ ├── ShellExecuteInfo.cs │ ├── TimeCaps.cs │ ├── TitlebarInfo.cs │ ├── WindowClass.cs │ ├── WindowInfo.cs │ └── WindowPlacement.cs ├── ScreenToGif.Test/ │ ├── A11Y/ │ │ └── ThemeContrastTests.cs │ ├── Data/ │ │ └── Upload/ │ │ └── Test.txt │ ├── Facts/ │ │ ├── ImageComparison.cs │ │ └── YandexUpload.cs │ ├── ScreenToGif.Test.csproj │ └── Util/ │ └── HttpHelper.cs ├── ScreenToGif.Util/ │ ├── Arguments.cs │ ├── BitHelper.cs │ ├── Codification/ │ │ ├── Apng/ │ │ │ ├── Apng.cs │ │ │ └── Chunks/ │ │ │ ├── ActlChunk.cs │ │ │ ├── ApngFrame.cs │ │ │ ├── Chunk.cs │ │ │ ├── FctlChunk.cs │ │ │ ├── FdatChunk.cs │ │ │ ├── IdatChunk.cs │ │ │ └── IhdrChunk.cs │ │ ├── Gif/ │ │ │ ├── Decoder/ │ │ │ │ ├── GifApplicationExtension.cs │ │ │ │ ├── GifBlock.cs │ │ │ │ ├── GifBlockKind.cs │ │ │ │ ├── GifColor.cs │ │ │ │ ├── GifCommentExtension.cs │ │ │ │ ├── GifDecoderException.cs │ │ │ │ ├── GifExtension.cs │ │ │ │ ├── GifFile.cs │ │ │ │ ├── GifFrame.cs │ │ │ │ ├── GifGraphicControlExtension.cs │ │ │ │ ├── GifHeader.cs │ │ │ │ ├── GifHelpers.cs │ │ │ │ ├── GifImageData.cs │ │ │ │ ├── GifImageDescriptor.cs │ │ │ │ ├── GifLogicalScreenDescriptor.cs │ │ │ │ ├── GifPlainTextExtension.cs │ │ │ │ └── GifTrailer.cs │ │ │ ├── Encoder/ │ │ │ │ ├── BitEncoder.cs │ │ │ │ ├── GifFile.cs │ │ │ │ ├── LZWEncoder.cs │ │ │ │ └── Quantization/ │ │ │ │ ├── GrayscaleQuantizer.cs │ │ │ │ ├── MedianCutQuantizer.cs │ │ │ │ ├── MostUsedQuantizer.cs │ │ │ │ ├── NeuralQuantizer.cs │ │ │ │ ├── OctreeQuantizer.cs │ │ │ │ ├── PaletteQuantizer.cs │ │ │ │ └── Quantizer.cs │ │ │ └── LegacyEncoder/ │ │ │ ├── GifEncoder.cs │ │ │ ├── LZWEncoder.cs │ │ │ └── PixelUtilOld.cs │ │ ├── PixelUtil.cs │ │ └── Psd/ │ │ ├── AditionalLayers/ │ │ │ ├── IAditionalLayerInfo.cs │ │ │ ├── IMetadata.cs │ │ │ ├── Metadata.cs │ │ │ └── MetadataContent.cs │ │ ├── Channel.cs │ │ ├── IPsdContent.cs │ │ ├── Image.cs │ │ ├── ImageChannelData.cs │ │ ├── ImageData.cs │ │ ├── ImageResourceBlocks/ │ │ │ ├── AnimationBlock.cs │ │ │ ├── IImageResource.cs │ │ │ └── ImageResources.cs │ │ ├── LayerAndMask.cs │ │ ├── LayerInfo.cs │ │ ├── LayerRecord.cs │ │ └── Psd.cs │ ├── Constants.cs │ ├── Converters/ │ │ ├── AlphaToOpacity.cs │ │ ├── BoolAnd.cs │ │ ├── BoolAndOrOrToVisibility.cs │ │ ├── BoolAndToVisibility.cs │ │ ├── BoolOr.cs │ │ ├── BoolOrAndToVisibility.cs │ │ ├── BoolOrToInvertedVisibility.cs │ │ ├── BoolOrToVisibility.cs │ │ ├── BoolToOpacity.cs │ │ ├── BytesToSize.cs │ │ ├── CenterPopupConverter.cs │ │ ├── ColorToAlpha.cs │ │ ├── ColorToBrush.cs │ │ ├── ContentToVisibility.cs │ │ ├── CountToBool.cs │ │ ├── DoubleTimesAHundredToInt.cs │ │ ├── DoubleToBool.cs │ │ ├── DoubleToInt.cs │ │ ├── DoubleToPercentage.cs │ │ ├── DoubleToPositionSize.cs │ │ ├── DoubleToRadius.cs │ │ ├── DoubleToThickness.cs │ │ ├── EnumToBool.cs │ │ ├── EnumToInt.cs │ │ ├── EnumToVisibility.cs │ │ ├── FontToSupportedGliph.cs │ │ ├── FormatConverter.cs │ │ ├── HasEnumToVisibility.cs │ │ ├── IntToBool.cs │ │ ├── IntToDecimal.cs │ │ ├── IntToDelayString.cs │ │ ├── IntToRect.cs │ │ ├── IntToString.cs │ │ ├── IntToVisibility.cs │ │ ├── InvertedBool.cs │ │ ├── InvertedBoolToVisibility.cs │ │ ├── InvertedEnumToBool.cs │ │ ├── InvertedEnumToVisibility.cs │ │ ├── InvertedIntToVisibility.cs │ │ ├── InvertedVisibility.cs │ │ ├── KeyToResource.cs │ │ ├── KeysToString.cs │ │ ├── MaximumValue.cs │ │ ├── MultiLineTitle.cs │ │ ├── NullToVisibility.cs │ │ ├── PathToFilename.cs │ │ ├── PercentageToOpacity.cs │ │ ├── ScaleConverter.cs │ │ ├── SelectionCountToDescription.cs │ │ ├── SelectionToDrawingAttributes.cs │ │ ├── SelectionToEditingMode.cs │ │ ├── SelectionToStylusShape.cs │ │ ├── ShortcutSelection.cs │ │ ├── SourceToSize.cs │ │ ├── StageToButtonString.cs │ │ ├── StageToCanvas.cs │ │ ├── StringArrayTypeConverter.cs │ │ ├── StringToDoubleArray.cs │ │ ├── StringToInt.cs │ │ ├── StylusTipToBool.cs │ │ ├── TagToSelection.cs │ │ ├── TimeSpanToString.cs │ │ ├── TimeSpanToTotalMilliseconds.cs │ │ └── UriToBitmap.cs │ ├── CrcHelper.cs │ ├── DataGridHelper.cs │ ├── DebounceDispatcher.cs │ ├── DirectoryHelper.cs │ ├── DynamicResourceBinding.cs │ ├── ExtendedStack.cs │ ├── Extensions/ │ │ ├── EnumExtensions.cs │ │ ├── ImageExtensions.cs │ │ ├── InlineExtensions.cs │ │ ├── MathExtensions.cs │ │ ├── ParseExtensions.cs │ │ ├── PropertyExtensions.cs │ │ ├── RectExtensions.cs │ │ ├── StringExtensions.cs │ │ └── VersionExtensions.cs │ ├── FastRandom.cs │ ├── Framerate.cs │ ├── GifskiInterop.cs │ ├── GitHubHelper.cs │ ├── Helpers/ │ │ ├── CursorHelper.cs │ │ ├── FfmpegHelper.cs │ │ └── KeyHelper.cs │ ├── Humanizer.cs │ ├── IdentityHelper.cs │ ├── InterProcessChannel/ │ │ ├── InstanceSwitcherChannel.cs │ │ ├── PipeServer.cs │ │ └── SettingsPersistenceChannel.cs │ ├── LocalizationHelper.cs │ ├── LogWritter.cs │ ├── MutexList.cs │ ├── Native/ │ │ ├── Capture.cs │ │ ├── HotKeyCollection.cs │ │ ├── InputHook.cs │ │ ├── Monitor.cs │ │ ├── NotifyIconHelper.cs │ │ └── WindowHelper.cs │ ├── NetworkHelper.cs │ ├── OperationalSystemHelper.cs │ ├── PathHelper.cs │ ├── ProcessHelper.cs │ ├── ScreenToGif.Util.csproj │ ├── Secret.cs │ ├── Serializer.cs │ ├── Settings/ │ │ ├── Migrations/ │ │ │ ├── Migration0to2_28_0.cs │ │ │ ├── Migration2_28_0To2_29_0.cs │ │ │ ├── Migration2_29_0To2_31_0.cs │ │ │ ├── Migration2_31_0To2_32_0.cs │ │ │ ├── Migration2_32_0To2_35_0.cs │ │ │ ├── Migration2_35_0To2_36_0.cs │ │ │ ├── Migration2_36_0To2_37_0.cs │ │ │ └── Migration2_37_0To2_43_0.cs │ │ ├── Migrations.cs │ │ └── UserSettings.cs │ ├── SimpleKeyGesture.cs │ ├── StreamHelpers.cs │ ├── UiElementsExtension.cs │ ├── VisualHelper.cs │ ├── WebHelper.cs │ └── WordLevel.cs └── ScreenToGif.ViewModel/ ├── BoardRecorderViewModel.cs ├── EditorViewModel.cs ├── ExportPresets/ │ ├── AnimatedImage/ │ │ ├── AnimatedImagePreset.cs │ │ ├── Apng/ │ │ │ ├── ApngPreset.cs │ │ │ ├── EmbeddedApngPreset.cs │ │ │ └── FfmpegApngPreset.cs │ │ ├── Avif/ │ │ │ ├── AvifPreset.cs │ │ │ └── FfmpegAvifPreset.cs │ │ ├── Bpg/ │ │ │ └── BpgPreset.cs │ │ ├── Gif/ │ │ │ ├── EmbeddedGifPreset.cs │ │ │ ├── FfmpegGifPreset.cs │ │ │ ├── GifPreset.cs │ │ │ ├── GifskiGifPreset.cs │ │ │ ├── KGySoftGifPreset.cs │ │ │ └── SystemGifPreset.cs │ │ └── Webp/ │ │ ├── FfmpegWebpPreset.cs │ │ └── WebpPreset.cs │ ├── ExportPreset.cs │ ├── Image/ │ │ ├── BmpPreset.cs │ │ ├── ImagePreset.cs │ │ ├── JpegPreset.cs │ │ └── PngPreset.cs │ ├── Other/ │ │ ├── PsdPreset.cs │ │ └── StgPreset.cs │ └── Video/ │ ├── Avi/ │ │ ├── AviPreset.cs │ │ └── FfmpegAviPreset.cs │ ├── Codecs/ │ │ ├── H264Amf.cs │ │ ├── H264Nvenc.cs │ │ ├── H264Qsv.cs │ │ ├── HevcAmf.cs │ │ ├── HevcNvenc.cs │ │ ├── HevcQsv.cs │ │ ├── LibAom.cs │ │ ├── Mpeg2.cs │ │ ├── Mpeg4.cs │ │ ├── Rav1E.cs │ │ ├── SvtAv1.cs │ │ ├── VideoCodec.cs │ │ ├── Vp8.cs │ │ ├── Vp9.cs │ │ ├── X264.cs │ │ └── X265.cs │ ├── Mkv/ │ │ ├── FfmpegMkvPreset.cs │ │ └── MkvPreset.cs │ ├── Mov/ │ │ ├── FfmpegMovPreset.cs │ │ └── MovPreset.cs │ ├── Mp4/ │ │ ├── FfmpegMp4Preset.cs │ │ └── Mp4Preset.cs │ ├── VideoPreset.cs │ └── Webm/ │ ├── FfmpegWebmPreset.cs │ └── WebmPreset.cs ├── FrameViewModel.cs ├── RecorderViewModel.cs ├── ScreenRecorderViewModel.cs ├── ScreenToGif.ViewModel.csproj ├── Settings/ │ └── PluginSettingsViewModel.cs ├── Tasks/ │ ├── BaseTaskViewModel.cs │ ├── BorderViewModel.cs │ ├── DelayViewModel.cs │ ├── KeyStrokesViewModel.cs │ ├── MouseEventsViewModel.cs │ ├── ProgressViewModel.cs │ ├── ResizeViewModel.cs │ └── ShadowViewModel.cs ├── UpdateAvailable.cs ├── UploadPresets/ │ ├── Custom/ │ │ └── CustomPreset.cs │ ├── History/ │ │ ├── History.cs │ │ └── ImgurHistory.cs │ ├── Imgur/ │ │ ├── ImgurAlbum.cs │ │ └── ImgurPreset.cs │ ├── UploadPreset.cs │ └── Yandex/ │ └── YandexPreset.cs ├── VideoSourceViewModel.cs └── WebcamViewModel.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true # All files [*] charset = utf-8 end_of_line = crlf insert_final_newline = false indent_style = space indent_size = 4 trim_trailing_whitespace = true # Markdown files [*.{md}] indent_size = 2 # Visual Studio Solution Files [*.sln] indent_style = tab # Visual Studio XML Project Files [*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 # XML Configuration Files [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] indent_size = 2 # C# files [*.cs] #### Core EditorConfig Options #### # Indentation and spacing indent_size = 4 indent_style = space tab_width = 4 # New line preferences end_of_line = crlf insert_final_newline = false #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = false file_header_template = unset # this. and Me. preferences dotnet_style_qualification_for_event = false:suggestion dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_property = false:suggestion # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity dotnet_style_parentheses_in_other_binary_operators = always_for_clarity dotnet_style_parentheses_in_other_operators = never_if_unnecessary dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members # Expression-level preferences dotnet_style_coalesce_expression = true dotnet_style_collection_initializer = true dotnet_style_explicit_tuple_names = true dotnet_style_namespace_match_folder = true dotnet_style_null_propagation = true dotnet_style_object_initializer = true dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true:suggestion dotnet_style_prefer_compound_assignment = true dotnet_style_prefer_conditional_expression_over_assignment = true dotnet_style_prefer_conditional_expression_over_return = true dotnet_style_prefer_inferred_anonymous_type_member_names = true dotnet_style_prefer_inferred_tuple_names = true dotnet_style_prefer_is_null_check_over_reference_equality_method = true dotnet_style_prefer_simplified_boolean_expressions = true dotnet_style_prefer_simplified_interpolation = true # Field preferences dotnet_style_readonly_field = true # Parameter preferences dotnet_code_quality_unused_parameters = all # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = 0 # New line preferences dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion dotnet_style_allow_statement_immediately_after_block_experimental = false:warning #### C# Coding Conventions #### # var preferences csharp_style_var_elsewhere = true:suggestion csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members csharp_style_expression_bodied_accessors = when_on_single_line:suggestion csharp_style_expression_bodied_constructors = true csharp_style_expression_bodied_indexers = true csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion csharp_style_expression_bodied_local_functions = false csharp_style_expression_bodied_methods = true csharp_style_expression_bodied_operators = true csharp_style_expression_bodied_properties = true # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true csharp_style_pattern_matching_over_is_with_cast_check = true csharp_style_prefer_not_pattern = true csharp_style_prefer_pattern_matching = true:suggestion csharp_style_prefer_switch_expression = true # Null-checking preferences csharp_style_conditional_delegate_call = true # Modifier preferences csharp_prefer_static_local_function = true csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async # Code-block preferences csharp_prefer_braces = false:suggestion csharp_prefer_simple_using_statement = true csharp_style_namespace_declarations = file_scoped:suggestion # Expression-level preferences csharp_prefer_simple_default_expression = true csharp_style_deconstructed_variable_declaration = true csharp_style_implicit_object_creation_when_type_is_apparent = true csharp_style_inlined_variable_declaration = true csharp_style_pattern_local_over_anonymous_function = true csharp_style_prefer_index_operator = true:silent csharp_style_prefer_null_check_over_type_check = true csharp_style_prefer_range_operator = true:silent csharp_style_throw_expression = true csharp_style_unused_value_assignment_preference = discard_variable csharp_style_unused_value_expression_statement_preference = discard_variable # 'using' directive preferences csharp_using_directive_placement = outside_namespace:warning # New line preferences csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning #### C# Formatting Rules #### # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = false csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #### Naming styles #### # Naming rules dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [NickeManarin] patreon: nicke open_collective: # Replace with a single Open Collective username ko_fi: nickemanarin tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: NickeManarin otechie: # Replace with a single Otechie username custom: ["https://www.screentogif.com/donate", "https://flattr.com/@NickeManarin/domain/screentogif.com", "https://www.buymeacoffee.com/NickeManarin"] ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug Report about: Create a report to help us improve title: "[Bug] Title" labels: "\U0001F537 Bug \U0001F41B, ⬜ Pending" assignees: NickeManarin --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows 10] - Version [e.g. 2.35.2] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature Request about: Suggest an idea for this project title: "[Feature Request] Title" labels: "\U0001F537Enhancement, ⬜ Pending" assignees: NickeManarin --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/discord-releases.yml ================================================ # This is a basic workflow to help you get started with Actions name: Discord • Releases # Controls when the action will run. on: # Triggers the workflow on new release. release: types: [published] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "released" released: # The type of runner that the job will run on runs-on: ubuntu-latest name: Sends release details to Discord/News channel # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Sending release notes # Checks-out a repository, to send the release notes uses: nhevia/discord-styled-releases@main # Gets the ID and Token from the project secrets with: webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} ================================================ FILE: .gitignore ================================================ ################################################################################ # This .gitignore file was automatically created by Microsoft(R) Visual Studio. ################################################################################ /GifRecorder.v12.suo /.vs /GifRecorder /GifRecorder.v12-Notebook-Nicke.suo /GifRecorder.suo /GifRecorder.sdf /GifRecorder.sln.DotSettings ScreenToGif/Util/Secret.cs /ScreenToGif/Util/Secret.cs /ScreenToGif.UWP /packages obj bin /log.txt *.user ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language, using english when possible * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at nicke@outlook.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ ## Rules to follow for this project: * Every feature should be packed into the main executable, unless it's optional like FFmpeg and Gifski. * To be accepted, any big feature or change should be discussed first with the maintainer of the project. * PRs should be directed to the dev branch. ================================================ FILE: Directory.Build.props ================================================ 2.43.0 2.43.0 2.43.0 Nicke Manarin Nicke Manarin Copyright© Nicke Manarin 2026 https://www.screentogif.com Readme.md https://github.com/nickemanarin/screentogif git gif; recorder; editor; screen-recorder; gif-editor MS-PL Screen, webcam and sketchboard recorder, with integrated editor! false ================================================ FILE: Directory.Packages.props ================================================ true ================================================ FILE: GifRecorder.sln ================================================ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.5.11605.296 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{0039724A-6303-4D73-B5B2-7063DF16C573}" ProjectSection(SolutionItems) = preProject CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props LICENSE.txt = LICENSE.txt LOCALIZATION.md = LOCALIZATION.md README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif", "ScreenToGif\ScreenToGif.csproj", "{9A332077-74BA-4C6A-8381-6D98C31A490A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Other", "Other", "{13F2A1B9-496A-446E-8B06-776ACAE5CEA4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Translator", "Other\Translator\Translator.csproj", "{8B516DFB-0981-48A2-8A06-35F085C13980}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif.Domain", "ScreenToGif.Model\ScreenToGif.Domain.csproj", "{EEE831AD-1447-474D-9875-94E56A854E71}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif.Native", "ScreenToGif.Native\ScreenToGif.Native.csproj", "{66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif.Test", "ScreenToGif.Test\ScreenToGif.Test.csproj", "{9D64714B-20BC-4A18-B89F-FA432E710EB4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif.Util", "ScreenToGif.Util\ScreenToGif.Util.csproj", "{B39A6DFB-F44E-403D-9451-3CEAD3423135}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenToGif.ViewModel", "ScreenToGif.ViewModel\ScreenToGif.ViewModel.csproj", "{97AAAA14-2793-49B7-96C5-6C6E83C55EB7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug (Store)|Any CPU = Debug (Store)|Any CPU Debug (Store)|ARM64 = Debug (Store)|ARM64 Debug (Store)|x64 = Debug (Store)|x64 Debug (Store)|x86 = Debug (Store)|x86 Debug|Any CPU = Debug|Any CPU Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|ARM64.Build.0 = Debug|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|x64.ActiveCfg = Debug|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|x64.Build.0 = Debug|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|x86.ActiveCfg = Debug|x86 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Debug|x86.Build.0 = Debug|x86 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|Any CPU.Build.0 = Release|Any CPU {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|ARM64.ActiveCfg = Release|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|ARM64.Build.0 = Release|ARM64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|x64.ActiveCfg = Release|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|x64.Build.0 = Release|x64 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|x86.ActiveCfg = Release|x86 {9A332077-74BA-4C6A-8381-6D98C31A490A}.Release|x86.Build.0 = Release|x86 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug|ARM64.ActiveCfg = Debug|ARM64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug|x64.ActiveCfg = Debug|x64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Debug|x86.ActiveCfg = Debug|x86 {8B516DFB-0981-48A2-8A06-35F085C13980}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Release|Any CPU.Build.0 = Release|Any CPU {8B516DFB-0981-48A2-8A06-35F085C13980}.Release|ARM64.ActiveCfg = Release|ARM64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Release|x64.ActiveCfg = Release|x64 {8B516DFB-0981-48A2-8A06-35F085C13980}.Release|x86.ActiveCfg = Release|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|Any CPU.Build.0 = Debug|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|ARM64.ActiveCfg = Debug|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|ARM64.Build.0 = Debug|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|x64.ActiveCfg = Debug|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|x64.Build.0 = Debug|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|x86.ActiveCfg = Debug|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Debug|x86.Build.0 = Debug|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|Any CPU.ActiveCfg = Release|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Release|Any CPU.Build.0 = Release|Any CPU {EEE831AD-1447-474D-9875-94E56A854E71}.Release|ARM64.ActiveCfg = Release|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|ARM64.Build.0 = Release|ARM64 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|x64.ActiveCfg = Release|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|x64.Build.0 = Release|x64 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|x86.ActiveCfg = Release|x86 {EEE831AD-1447-474D-9875-94E56A854E71}.Release|x86.Build.0 = Release|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|Any CPU.Build.0 = Debug|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|ARM64.ActiveCfg = Debug|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|ARM64.Build.0 = Debug|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|x64.ActiveCfg = Debug|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|x64.Build.0 = Debug|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|x86.ActiveCfg = Debug|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Debug|x86.Build.0 = Debug|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|Any CPU.ActiveCfg = Release|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|Any CPU.Build.0 = Release|Any CPU {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|ARM64.ActiveCfg = Release|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|ARM64.Build.0 = Release|ARM64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|x64.ActiveCfg = Release|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|x64.Build.0 = Release|x64 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|x86.ActiveCfg = Release|x86 {66D60F4A-C0B4-4077-8DE6-0431F6AD5E87}.Release|x86.Build.0 = Release|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|ARM64.Build.0 = Debug|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|x64.ActiveCfg = Debug|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|x64.Build.0 = Debug|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|x86.ActiveCfg = Debug|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Debug|x86.Build.0 = Debug|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|Any CPU.Build.0 = Release|Any CPU {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|ARM64.ActiveCfg = Release|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|ARM64.Build.0 = Release|ARM64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|x64.ActiveCfg = Release|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|x64.Build.0 = Release|x64 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|x86.ActiveCfg = Release|x86 {9D64714B-20BC-4A18-B89F-FA432E710EB4}.Release|x86.Build.0 = Release|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|Any CPU.Build.0 = Debug|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|ARM64.ActiveCfg = Debug|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|ARM64.Build.0 = Debug|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|x64.ActiveCfg = Debug|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|x64.Build.0 = Debug|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|x86.ActiveCfg = Debug|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Debug|x86.Build.0 = Debug|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|Any CPU.ActiveCfg = Release|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|Any CPU.Build.0 = Release|Any CPU {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|ARM64.ActiveCfg = Release|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|ARM64.Build.0 = Release|ARM64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|x64.ActiveCfg = Release|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|x64.Build.0 = Release|x64 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|x86.ActiveCfg = Release|x86 {B39A6DFB-F44E-403D-9451-3CEAD3423135}.Release|x86.Build.0 = Release|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|Any CPU.ActiveCfg = Debug (Store)|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|Any CPU.Build.0 = Debug (Store)|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|ARM64.ActiveCfg = Debug (Store)|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|ARM64.Build.0 = Debug (Store)|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|x64.ActiveCfg = Debug (Store)|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|x64.Build.0 = Debug (Store)|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|x86.ActiveCfg = Debug (Store)|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug (Store)|x86.Build.0 = Debug (Store)|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|ARM64.ActiveCfg = Debug|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|ARM64.Build.0 = Debug|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|x64.ActiveCfg = Debug|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|x64.Build.0 = Debug|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|x86.ActiveCfg = Debug|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Debug|x86.Build.0 = Debug|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|Any CPU.Build.0 = Release|Any CPU {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|ARM64.ActiveCfg = Release|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|ARM64.Build.0 = Release|ARM64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|x64.ActiveCfg = Release|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|x64.Build.0 = Release|x64 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|x86.ActiveCfg = Release|x86 {97AAAA14-2793-49B7-96C5-6C6E83C55EB7}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {8B516DFB-0981-48A2-8A06-35F085C13980} = {13F2A1B9-496A-446E-8B06-776ACAE5CEA4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E505E312-14B9-49C0-AC18-B3B2FB6C1661} EndGlobalSection EndGlobal ================================================ FILE: LICENSE.txt ================================================ Microsoft Public License (Ms-PL) This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. 1. Definitions The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. A "contribution" is the original software, or any additions or changes to the software. A "contributor" is any person that distributes its contribution under this license. "Licensed patents" are a contributor's patent claims that read directly on its contribution. 2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. 3. Conditions and Limitations (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. ================================================ FILE: LOCALIZATION.md ================================================ # Localization ScreenToGif's base language is [English](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.en.xaml).
The typical workflow for translation work comprises three key stages: translating, testing, and submission. To contribute to the translation of ScreenToGif, it is advisable to first review the guidelines provided in the wiki: - [Translating the App](https://github.com/NickeManarin/ScreenToGif/wiki/Localization) - [Translating the Installer](https://github.com/NickeManarin/ScreenToGif/wiki/Localization-%28Installer%29) - [Translating the Website](https://github.com/NickeManarin/ScreenToGif/wiki/Localization-%28Website%29) As this project always are moving forward, new strings get added from time to time which means they need to be added to the language files and be translated. Anyone can still contribute to the languages. --- ## Language Status | Language | Maintainer | Status | | -------- | ---------- | ----------- | | [![](https://img.shields.io/badge/ar-maintained-orange.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.ar.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/cs-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.cs.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/da-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.da.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/de-maintained-green.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.de.xaml) | [panther2](https://github.com/panther2) | Updated | | [![](https://img.shields.io/badge/en-maintained-brightgreen.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.en.xaml) | [NickeManarin](https://github.com/NickeManarin) | Base language | | [![](https://img.shields.io/badge/es--AR-maintained-brightgreen.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.es-AR.xaml) | [NickeManarin](https://github.com/NickeManarin) | Updated | | [![](https://img.shields.io/badge/es-maintained-green.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.es.xaml) | [NickeManarin](https://github.com/NickeManarin) | Updated | | [![](https://img.shields.io/badge/fr-maintained-green.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.fr.xaml) | [Tr4ncer](https://github.com/Tr4ncer) | Updated | | [![](https://img.shields.io/badge/it-maintained-green.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.it.xaml) | [mlocati](https://github.com/mlocati) | Updated | | [![](https://img.shields.io/badge/ja-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.ja.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/ko-maintained-orange.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.ko.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/nl-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.nl.xaml) | [Stephan-P](https://github.com/Stephan-P) | Updated | | [![](https://img.shields.io/badge/pl-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.pl.xaml) | [spietras](https://github.com/spietras) | Updated | | [![](https://img.shields.io/badge/pt--PT-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.pt-PT.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/pt-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.pt.xaml) | [NickeManarin](https://github.com/NickeManarin) | Updated | | [![](https://img.shields.io/badge/ru-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.ru.xaml) | [om2804](https://github.com/om2804) | Updated | | [![](https://img.shields.io/badge/sv-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.sv.xaml) | [anixsson](https://github.com/anixsson) | Updated | | [![](https://img.shields.io/badge/tr-maintained-orange.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.tr.xaml) | [mollamehmetoglu](https://github.com/mollamehmetoglu) | Need help | | [![](https://img.shields.io/badge/uk-maintained-orange.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.uk.xaml) | [NickeManarin](https://github.com/NickeManarin) | Need help | | [![](https://img.shields.io/badge/zh--Hant-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.zh--Hant.xaml) | [spietras](https://github.com/spietras) | Updated | | [![](https://img.shields.io/badge/zh-maintained-yellow.svg)](https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Resources/Localization/StringResources.zh.xaml) | [spietras](https://github.com/spietras) | Updated | ================================================ FILE: Other/Translator/App.xaml ================================================ ================================================ FILE: Other/Translator/App.xaml.cs ================================================ using System; using System.Reflection; using System.Windows; using System.Windows.Threading; using Translator.Util; namespace Translator; public partial class App : Application { private void App_Startup(object sender, StartupEventArgs e) { //Unhandled Exceptions. AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; } private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { LogWriter.Log(e.Exception, "On Dispatcher Unhandled Exception - Unknown"); try { ExceptionDialog.Ok(e.Exception, "ScreenToGif - Translator", "Unhandled exception", e.Exception.Message); } catch (Exception ex) { LogWriter.Log(ex, "Error while displaying the error."); //Ignored. } e.Handled = true; } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is not Exception exception) return; LogWriter.Log(exception, "Current Domain Unhandled Exception - Unknown"); try { ExceptionDialog.Ok(exception, "ScreenToGif - Translator", "Unhandled exception", exception.Message); } catch (Exception) { //Ignored. } } public static string Version => ToStringShort(Assembly.GetEntryAssembly()?.GetName().Version) ?? "0.0"; internal static string ToStringShort(Version version) { if (version == null) return null; var result = $"{version.Major}.{version.Minor}"; if (version.Build > 0) result += $".{version.Build}"; if (version.Revision > 0) result += $".{version.Revision}"; return result; } } ================================================ FILE: Other/Translator/Controls/ExtendedTextBox.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Translator.Controls; public class ExtendedTextBox : TextBox { static ExtendedTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedTextBox), new FrameworkPropertyMetadata(typeof(ExtendedTextBox))); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); SelectAll(); } } ================================================ FILE: Other/Translator/Controls/ImageButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; namespace Translator.Controls; /// /// Button with a image inside. /// public class ImageButton : Button { #region Variables public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ImageButton), new FrameworkPropertyMetadata("Button")); public static readonly DependencyProperty MaxSizeProperty = DependencyProperty.Register("MaxSize", typeof(double), typeof(ImageButton), new FrameworkPropertyMetadata(26.0)); public static readonly DependencyProperty KeyGestureProperty = DependencyProperty.Register("KeyGesture", typeof(string), typeof(ImageButton), new FrameworkPropertyMetadata("")); /// /// DependencyProperty for property. /// public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(ImageButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties /// /// The text of the button. /// [Description("The text of the button."), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image."), Category("Common")] public double MaxSize { get => (double)GetValue(MaxSizeProperty); set => SetCurrentValue(MaxSizeProperty, value); } /// /// The KeyGesture of the button. /// [Description("The KeyGesture of the button."), Category("Common")] public string KeyGesture { get => (string)GetValue(KeyGestureProperty); set => SetCurrentValue(KeyGestureProperty, value); } /// /// The TextWrapping property controls whether or not text wraps /// when it reaches the flow edge of its containing block box. /// public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } #endregion static ImageButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton), new FrameworkPropertyMetadata(typeof(ImageButton))); } } ================================================ FILE: Other/Translator/Controls/ImageMenuItem.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; namespace Translator.Controls; /// /// MenuItem with an image to the left. /// public class ImageMenuItem : MenuItem { #region Variables public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("Image", typeof(UIElement), typeof(ImageMenuItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty MaxSizeProperty = DependencyProperty.Register("MaxSize", typeof(double), typeof(ImageMenuItem), new FrameworkPropertyMetadata(15.0)); public static readonly DependencyProperty HasImageProperty = DependencyProperty.Register("HasImage", typeof(bool), typeof(ImageMenuItem), new FrameworkPropertyMetadata(false)); #endregion #region Properties /// /// The Image of the button. /// [Description("The Image of the button.")] public UIElement Image { get { return (UIElement)GetValue(ImageProperty); } set { SetCurrentValue(ImageProperty, value); //Has Image. SetCurrentValue(HasImageProperty, value != null); } } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double MaxSize { get { return (double)GetValue(MaxSizeProperty); } set { SetCurrentValue(MaxSizeProperty, value); } } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public bool HasImage { get { return (bool)GetValue(HasImageProperty); } set { SetCurrentValue(HasImageProperty, value); } } #endregion static ImageMenuItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageMenuItem), new FrameworkPropertyMetadata(typeof(ImageMenuItem))); } } ================================================ FILE: Other/Translator/Controls/StatusBand.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; namespace Translator.Controls; public class StatusBand : Control { #region Variables public enum StatusTypes { Info, Warning, Error } private Grid _warningGrid; private Button _supressButton; #endregion #region Dependency Properties public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type", typeof(StatusTypes), typeof(StatusBand), new FrameworkPropertyMetadata(StatusTypes.Warning, OnTypePropertyChanged)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(StatusBand), new FrameworkPropertyMetadata("", OnTextPropertyChanged)); public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("Image", typeof(UIElement), typeof(StatusBand), new FrameworkPropertyMetadata(null, OnImagePropertyChanged)); public static readonly DependencyProperty StartingProperty = DependencyProperty.Register("Starting", typeof(bool), typeof(StatusBand), new PropertyMetadata(default(bool))); #endregion #region Properties [Bindable(true), Category("Common")] public StatusTypes Type { get => (StatusTypes)GetValue(TypeProperty); set => SetValue(TypeProperty, value); } [Bindable(true), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } [Bindable(true), Category("Common")] public UIElement Image { get => (UIElement)GetValue(ImageProperty); set => SetValue(ImageProperty, value); } /// /// True if started to display the message. /// [Bindable(true), Category("Common")] public bool Starting { get => (bool)GetValue(StartingProperty); set => SetValue(StartingProperty, value); } #endregion #region Property Changed private static void OnTypePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not StatusBand band) return; band.Type = (StatusTypes)e.NewValue; band.Image = (Canvas)band.FindResource(band.Type == StatusTypes.Info ? "Vector.Info" : band.Type == StatusTypes.Warning ? "Vector.Warning" : "Vector.Error"); } private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not StatusBand band) return; band.Text = (string)e.NewValue; } private static void OnImagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not StatusBand band) return; band.Image = (UIElement)e.NewValue; } #endregion static StatusBand() { DefaultStyleKeyProperty.OverrideMetadata(typeof(StatusBand), new FrameworkPropertyMetadata(typeof(StatusBand))); } public override void OnApplyTemplate() { _warningGrid = GetTemplateChild("WarningGrid") as Grid; _supressButton = GetTemplateChild("SuppressButton") as ImageButton; if (_supressButton != null) { _supressButton.Click += SupressButton_Click; } base.OnApplyTemplate(); } #region Methods public void Show(StatusTypes type, string text, UIElement image = null) { //Collapsed-by-default elements do not apply templates. //http://stackoverflow.com/a/2115873/1735672 //So it's necessary to do this here. ApplyTemplate(); Starting = true; Type = type; Text = text; Image = image; if (_warningGrid?.FindResource("ShowWarningStoryboard") is Storyboard show) BeginStoryboard(show); } public void Info(string text, UIElement image = null) { Show(StatusTypes.Info, text, image ?? (Canvas)FindResource("Vector.Info")); } public void Warning(string text, UIElement image = null) { Show(StatusTypes.Warning, text, image ?? (Canvas)FindResource("Vector.Warning")); } public void Error(string text, UIElement image = null) { Show(StatusTypes.Error, text, image ?? (Canvas)FindResource("Vector.Error")); } public void Hide() { Starting = false; if (_warningGrid?.Visibility == Visibility.Collapsed) return; if (_warningGrid?.FindResource("HideWarningStoryboard") is Storyboard show) BeginStoryboard(show); } #endregion private void SupressButton_Click(object sender, RoutedEventArgs e) { Hide(); } } ================================================ FILE: Other/Translator/Converters/MultiLineTitle.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace Translator.Converters; public class MultiLineTitle : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var text = value as string; return string.IsNullOrEmpty(text) ? value : text.Replace(@"\n", Environment.NewLine); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } ================================================ FILE: Other/Translator/Converters/NullToInvertedBool.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace Translator.Converters; public class NullToInvertedBool : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value == null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return Binding.DoNothing; } } ================================================ FILE: Other/Translator/Dialog.xaml ================================================  ================================================ FILE: Other/Translator/Dialog.xaml.cs ================================================ using System; using System.Windows; using System.Windows.Controls; namespace Translator; /// /// Interaction logic for Dialog.xaml /// public partial class Dialog : Window { /// /// Default constructor. /// public Dialog() { InitializeComponent(); } #region Methods private Canvas GetIcon(Icons icon) { switch (icon) { case Icons.Error: return (Canvas)FindResource("Vector.Error"); case Icons.Info: return (Canvas)FindResource("Vector.Info"); case Icons.Success: return (Canvas)FindResource("Vector.Success"); case Icons.Warning: return (Canvas)FindResource("Vector.Warning"); case Icons.Question: return (Canvas)FindResource("Vector.Question"); default: return (Canvas)FindResource("Vector.Info"); } } private void PrepareOk(string title, string instruction, string observation, Icons icon) { CancelButton.Visibility = Visibility.Collapsed; YesButton.Visibility = Visibility.Collapsed; NoButton.Visibility = Visibility.Collapsed; OkButton.Focus(); IconViewbox.Child = GetIcon(icon); InstructionLabel.Content = instruction; ObservationTextBlock.Text = observation; Title = title; } private void PrepareOkCancel(string title, string instruction, string observation, Icons icon) { YesButton.Visibility = Visibility.Collapsed; NoButton.Visibility = Visibility.Collapsed; CancelButton.Focus(); IconViewbox.Child = GetIcon(icon); InstructionLabel.Content = instruction; ObservationTextBlock.Text = observation; Title = title; } private void PrepareAsk(string title, string instruction, string observation, Icons icon) { CancelButton.Visibility = Visibility.Collapsed; OkButton.Visibility = Visibility.Collapsed; NoButton.Focus(); IconViewbox.Child = GetIcon(icon); InstructionLabel.Content = instruction; ObservationTextBlock.Text = observation; Title = title; } /// /// Shows a Ok dialog. /// /// The title of the window. /// The main instruction. /// A complementar observation. /// The image of the dialog. /// True if Ok public static bool Ok(string title, string instruction, string observation, Icons icon = Icons.Error) { var dialog = new Dialog(); dialog.PrepareOk(title, instruction, observation.Replace(@"\n", Environment.NewLine).Replace(@"\r", ""), icon); var result = dialog.ShowDialog(); return result.HasValue && result.Value; } /// /// Shows a Ok/Cancel dialog. /// /// The title of the window. /// The main instruction. /// A complementar observation. /// The image of the dialog. /// True if Ok public static bool OkCancel(string title, string instruction, string observation, Icons icon = Icons.Error) { var dialog = new Dialog(); dialog.PrepareOkCancel(title, instruction, observation.Replace(@"\n", Environment.NewLine).Replace(@"\r", ""), icon); var result = dialog.ShowDialog(); return result.HasValue && result.Value; } /// /// Shows a Yes/No dialog. /// /// The title of the window. /// The main instruction. /// A complementar observation. /// The image of the dialog. /// True if Yes public static bool Ask(string title, string instruction, string observation, Icons icon = Icons.Question) { var dialog = new Dialog(); dialog.PrepareAsk(title, instruction, observation.Replace(@"\n", Environment.NewLine).Replace(@"\r", ""), icon); var result = dialog.ShowDialog(); return result.HasValue && result.Value; } #endregion #region Events private void FalseActionButton_Click(object sender, RoutedEventArgs e) { DialogResult = false; } private void TrueActionButton_Click(object sender, RoutedEventArgs e) { DialogResult = true; } #endregion /// /// Dialog Icons. /// public enum Icons { /// /// Information. Blue. /// Info, /// /// Warning, yellow. /// Warning, /// /// Error, red. /// Error, /// /// Success, green. /// Success, /// /// A question mark, blue. /// Question, } } ================================================ FILE: Other/Translator/ExceptionDialog.xaml ================================================  ================================================ FILE: Other/Translator/ExceptionDialog.xaml.cs ================================================ using System; using System.Diagnostics; using System.Windows; using System.Windows.Documents; using Translator.Util; namespace Translator; public partial class ExceptionDialog : Window { #region Properties public bool BugWithHotFix4055002 { get; set; } public Exception Exception { get; set; } #endregion public ExceptionDialog(Exception exception) { InitializeComponent(); Exception = exception; } #region Eventos private void Window_Loaded(object sender, RoutedEventArgs e) { if (Exception == null) DetailsButton.IsEnabled = false; } private void DetailsButton_Click(object sender, RoutedEventArgs e) { var errorViewer = new ExceptionViewer(Exception); errorViewer.ShowDialog(); } private void OkButton_Click(object sender, RoutedEventArgs e) { Close(); } #endregion #region Métodos private void PrepareOk(string title, string instruction, string observation) { TypeTextBlock.Text = instruction; DetailsTextBlock.Inlines.Add(new Run("\t" + observation)); Title = title ?? "ScreenToGif - Error"; if (BugWithHotFix4055002) { DetailsTextBlock.Inlines.Add(new LineBreak()); DetailsTextBlock.Inlines.Add(new LineBreak()); DetailsTextBlock.Inlines.Add(new Run("\tThis was likely caused by a bug with an update for .Net Framework 4.7.1 (KB4055002, released in January 2018). This bug happens on machines with Windows 7 SP1 or Windows Server 2008 R2.")); DetailsTextBlock.Inlines.Add(new LineBreak()); DetailsTextBlock.Inlines.Add(new LineBreak()); DetailsTextBlock.Inlines.Add(new Run("\t")); var hyper = new Hyperlink(new Run("Click here to open a page with some details on how to fix this issue.") {ToolTip = "https://github.com/dotnet/announcements/issues/53" }); hyper.Click += HyperOnClick; DetailsTextBlock.Inlines.Add(hyper); } OkButton.Focus(); } private void HyperOnClick(object sender, RoutedEventArgs routedEventArgs) { try { Process.Start("https://github.com/dotnet/announcements/issues/53"); } catch (Exception e) { LogWriter.Log(e, "Impossible to open link"); } } #endregion #region Static Methods public static bool Ok(Exception exception, string title, string instruction, string observation = "", bool bugWith4055002 = false) { var dialog = new ExceptionDialog(exception) { BugWithHotFix4055002 = bugWith4055002 }; dialog.PrepareOk(title, instruction, observation); var result = dialog.ShowDialog(); return result.HasValue && result.Value; } #endregion } ================================================ FILE: Other/Translator/ExceptionViewer.xaml ================================================  ================================================ FILE: Other/Translator/ExceptionViewer.xaml.cs ================================================ using System; using System.Windows; namespace Translator; public partial class ExceptionViewer { #region Variables private readonly Exception _exception; #endregion /// /// Default constructor. /// /// The Exception to show. public ExceptionViewer(Exception ex) { InitializeComponent(); _exception = ex; #region Shows Information TypeLabel.Content = ex.GetType().Name; MessageTextBox.Text = ex.Message; StackTextBox.Text = ex.StackTrace; SourceTextBox.Text = ex.Source; if (ex.TargetSite != null) SourceTextBox.Text += "." + ex.TargetSite.Name; //If there's additional details. if (!string.IsNullOrEmpty(ex.HelpLink)) StackTextBox.Text += Environment.NewLine + Environment.NewLine + ex.HelpLink; if (ex.InnerException != null) InnerButton.IsEnabled = true; #endregion } private void InnerButton_Click(object sender, RoutedEventArgs e) { var errorViewer = new ExceptionViewer(_exception.InnerException); errorViewer.ShowDialog(); GC.Collect(1); } private void DoneButton_Click(object sender, RoutedEventArgs e) { Close(); } } ================================================ FILE: Other/Translator/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; using System.Windows; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("ScreenToGif Translator")] [assembly: AssemblyDescription("The translator tool for ScreenToGif")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Nicke Manarin")] [assembly: AssemblyProduct("ScreenToGif Translator")] [assembly: AssemblyCopyright("Copyright © Nicke Manarin 2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] //In order to begin building localizable applications, set //CultureYouAreCodingWith in your .csproj file //inside a . For example, if you are using US english //in your source files, set the to en-US. Then uncomment //the NeutralResourceLanguage attribute below. Update the "en-US" in //the line below to match the UICulture setting in the project file. //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located //(used if a resource is not found in the page, // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.3.0.0")] [assembly: AssemblyFileVersion("1.3.0.0")] ================================================ FILE: Other/Translator/Themes/Buttons.xaml ================================================ ================================================ FILE: Other/Translator/Themes/Colors.xaml ================================================  ================================================ FILE: Other/Translator/Themes/ComboBox.xaml ================================================  ================================================ FILE: Other/Translator/Themes/DataGridStyle.xaml ================================================  #FF002FA7 #FF002FA7 #FF007CBC #FF5700FF #FF002Fa7 Transparent Transparent ================================================ FILE: Other/Translator/Themes/Generic.xaml ================================================  ================================================ FILE: Other/Translator/Themes/IconSet.xaml ================================================  ================================================ FILE: Other/Translator/Themes/ProgressBar.xaml ================================================  ================================================ FILE: Other/Translator/Translator.csproj ================================================ net8.0-windows WinExe false true true AnyCPU;ARM64;x64;x86 Debug;Release;Debug (Store) Logo.ico all ================================================ FILE: Other/Translator/TranslatorWindow.xaml ================================================  ================================================ FILE: Other/Translator/TranslatorWindow.xaml.cs ================================================ using Microsoft.Win32; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Runtime.Serialization.Json; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Navigation; using System.Windows.Threading; using System.Xml.Linq; using System.Xml.XPath; using Translator.Util; using XamlReader = System.Windows.Markup.XamlReader; namespace Translator; public partial class TranslatorWindow : Window { private readonly List _resourceList = new(); private IEnumerable _cultures; private ObservableCollection _translationList = new(); private string _tempPath; private string _resourceTemplate; public TranslatorWindow() { InitializeComponent(); } #region Events private async void Window_Loaded(object sender, RoutedEventArgs e) { PrepareTempPath(); OpenButton.IsEnabled = false; RefreshButton.IsEnabled = false; ToComboBox.IsEnabled = false; #region Languages FromComboBox.Text = "Loading..."; ToComboBox.Text = "Loading..."; StatusBand.Info("Downloading English resource file..."); //We have to get english resource first in case we import first without refreshing await DownloadSingleResourceAsync("en"); StatusBand.Info("Loading language codes..."); _cultures = await GetProperCulturesAsync(); var languageList = await Task.Factory.StartNew(() => _cultures.Select(x => new Culture { Code = x, Name = CultureInfo.GetCultureInfo(x).DisplayName }).ToList()); //var languageList = CultureInfo.GetCultures(CultureTypes.AllCultures).Select(x => new Culture { Code = x.IetfLanguageTag, Name = x.EnglishName }).ToList(); FromComboBox.ItemsSource = languageList; ToComboBox.ItemsSource = languageList; ToComboBox.Text = null; FromComboBox.SelectedIndex = languageList.FindIndex(x => x.Code == "en"); StatusBand.Hide(); #endregion OpenButton.IsEnabled = true; RefreshButton.IsEnabled = true; ToComboBox.IsEnabled = true; ToComboBox.Focus(); } private void TutorialHyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) { try { Process.Start("https://github.com/NickeManarin/ScreenToGif/wiki/Localization"); } catch (Exception ex) { Dialog.Ok("Translator", "Tutorial", "Error while trying to open the tutorial link"); } } private void NewLineHyperlink_Click(object sender, RoutedEventArgs e) { Clipboard.SetText(" "); } private void ComboBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Return || e.Key == Key.Enter) { e.Handled = true; RefreshButton.Focus(); } } private async void Refresh_Click(object sender, RoutedEventArgs e) { var baseCulture = FromComboBox.SelectedValue as string; if (ToComboBox.SelectedValue is not string specificCulture) { StatusBand.Info("You need to select a target language to load the translations."); return; } HeaderLabel.Content = "Downloading resources..."; StatusBand.Info("Downloading selected translations..."); await DownloadResourcesAsync(baseCulture, specificCulture); ShowTranslations(baseCulture, specificCulture); HeaderLabel.Content = "Translator"; BaseDataGrid.IsEnabled = true; StatusBand.Hide(); } private void Itens_GotFocus(object sender, RoutedEventArgs e) { if (e.OriginalSource is not TextBox ue) return; ue.Dispatcher.BeginInvoke(DispatcherPriority.Send, () => ue.SelectAll()); BaseDataGrid.SelectedItem = ((FrameworkElement)sender).DataContext; } private void Item_PreviewKeyDown(object sender, KeyEventArgs e) { var source = e.OriginalSource as TextBox; if (source == null) return; //Back, up. if (e.Key == Key.Up || e.Key is Key.Enter or Key.Return && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))) { source.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up)); BaseDataGrid.BeginEdit(); var current = DataGridHelper.GetDataGridCell(BaseDataGrid.CurrentCell); current?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up)); e.Handled = true; return; } //Back, left. if ((e.Key == Key.Left && (source.CaretIndex == 0 || source.IsReadOnly)) || (e.Key == Key.Tab && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)))) { source.MoveFocus(new TraversalRequest(FocusNavigationDirection.Left)); BaseDataGrid.BeginEdit(); var current = DataGridHelper.GetDataGridCell(BaseDataGrid.CurrentCell); current?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Left)); e.Handled = true; return; } //Next, down. if (e.Key == Key.Down || e.Key == Key.Enter || e.Key == Key.Return) { source.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down)); BaseDataGrid.BeginEdit(); var current = DataGridHelper.GetDataGridCell(BaseDataGrid.CurrentCell); current?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down)); e.Handled = true; return; } //Next, right. OLD (e.Key == Key.Right && (source.CaretIndex == source.Text.Length - 1 || source.IsReadOnly)) || if (e.Key == Key.Tab) { source.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); BaseDataGrid.BeginEdit(); var current = DataGridHelper.GetDataGridCell(BaseDataGrid.CurrentCell); current?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); e.Handled = true; return; } } private void Load_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void Export_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = BaseDataGrid.IsEnabled && ToComboBox.SelectedValue != null && BaseDataGrid.Items.Count > 0; } private async void Load_Executed(object sender, ExecutedRoutedEventArgs e) { var ofd = new OpenFileDialog { AddExtension = true, CheckFileExists = true, Title = "Open a Resource Dictionary", Filter = "Resource Dictionary (*.xaml)|*.xaml;", InitialDirectory = Path.GetFullPath(_tempPath) }; var result = ofd.ShowDialog(); if (!result.HasValue || !result.Value) return; //Will save the file to other folder. var tempFile = Path.Combine(_tempPath, "Temp", Path.GetFileName(ofd.FileName)); Directory.CreateDirectory(Path.Combine(_tempPath, "Temp")); //Replaces the special chars. var text = await Task.Factory.StartNew(() => File.ReadAllText(ofd.FileName, Encoding.UTF8).Replace("&#", "&#").Replace("-->", "-->")); await Task.Factory.StartNew(() => File.WriteAllText(tempFile, text, Encoding.UTF8)); var dictionary = await Task.Factory.StartNew(() => new ResourceDictionary { Source = new Uri(Path.GetFullPath(tempFile), UriKind.Absolute) }); _resourceList.Add(dictionary); var baseCulture = FromComboBox.SelectedValue as string; var specificCulture = Path.GetFileName(ofd.FileName).Replace("StringResources.", "").Replace(".xaml", ""); string properCulture; //Catching here, because we can access UI thread easily here to show dialogs try { properCulture = await Task.Factory.StartNew(() => CheckSupportedCulture(specificCulture)); } catch (CultureNotFoundException) { Dialog.Ok("Action Denied", "Unknown Language.", $"The \"{specificCulture}\" and its family were not recognized as a valid language codes."); return; } catch (Exception ex) { Dialog.Ok("Action Denied", "Error checking culture.", ex.Message); return; } if (properCulture != specificCulture) { Dialog.Ok("Action Denied", "Redundant Language Code.", $"The \"{specificCulture}\" code is redundant. Try using \'{properCulture}\" instead"); return; } ToComboBox.SelectedValue = specificCulture; ShowTranslations(baseCulture, specificCulture); BaseDataGrid.IsEnabled = true; } private async void Export_Executed(object sender, ExecutedRoutedEventArgs e) { var sfd = new SaveFileDialog { AddExtension = true, Filter = "Resource Dictionary (*.xaml)|*.xaml", Title = "Save Resource Dictionary", FileName = $"StringResources.{ToComboBox.SelectedValue}.xaml" }; var result = sfd.ShowDialog(); if (!result.HasValue || !result.Value) return; BaseDataGrid.IsEnabled = false; StatusBand.Info("Exporting translation..."); var fileName = sfd.FileName; var saved = await Task.Factory.StartNew(() => ExportTranslation(fileName)); BaseDataGrid.IsEnabled = true; if (saved) StatusBand.Info("Translation saved!"); else StatusBand.Hide(); } private void CancelButton_Click(object sender, RoutedEventArgs e) { Close(); } private void Window_Closing(object sender, CancelEventArgs e) { if (BaseDataGrid.Items.Count > 0 && !Dialog.Ask("Translator", "Do you really wish to close?", "Don't forget to export your translation, if you started translating but not exported yet.")) e.Cancel = true; } #endregion #region Methods private void PrepareTempPath() { _tempPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScreenToGif", "Resources"); if (!Directory.Exists(_tempPath)) Directory.CreateDirectory(_tempPath); } private async Task DownloadSingleResourceAsync(string culture) { try { var request = (HttpWebRequest)WebRequest.Create("https://api.github.com/repos/NickeManarin/ScreenToGif/contents/ScreenToGif/Resources/Localization"); request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; var response = (HttpWebResponse)await request.GetResponseAsync(); await using (var resultStream = response.GetResponseStream()) { using (var reader = new StreamReader(resultStream)) { var result = await reader.ReadToEndAsync(); var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(result), new System.Xml.XmlDictionaryReaderQuotas()); var json = await Task.Factory.StartNew(() => XElement.Load(jsonReader)); var element = json.XPathSelectElement("/").Elements().FirstOrDefault(x => x.XPathSelectElement("name").Value.EndsWith(culture + ".xaml")); if (element == null) throw new WebException("File not found"); var name = element.XPathSelectElement("name").Value; var downloadUrl = element.XPathSelectElement("download_url").Value; await DownloadFileAsync(new Uri(downloadUrl), name); CommandManager.InvalidateRequerySuggested(); } } } catch (WebException web) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading Single Resource", web.Message + Environment.NewLine + "Trying to load files already downloaded.")); await LoadFilesAsync(); } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading Single Resource", ex.Message)); } GC.Collect(); } private async Task DownloadResourcesAsync(string baseCulture, string specificCulture) { try { var request = (HttpWebRequest)WebRequest.Create("https://api.github.com/repos/NickeManarin/ScreenToGif/contents/ScreenToGif/Resources/Localization"); request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; var response = (HttpWebResponse)await request.GetResponseAsync(); await using (var resultStream = response.GetResponseStream()) { using (var reader = new StreamReader(resultStream)) { var result = await reader.ReadToEndAsync(); var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(result), new System.Xml.XmlDictionaryReaderQuotas()); var json = await Task.Factory.StartNew(() => XElement.Load(jsonReader)); foreach (var element in json.XPathSelectElement("/").Elements()) { var name = element.XPathSelectElement("name").Value; if (string.IsNullOrEmpty(name) || (!name.EndsWith(baseCulture + ".xaml") && !name.EndsWith(specificCulture + ".xaml"))) continue; var downloadUrl = element.XPathSelectElement("download_url").Value; await DownloadFileAsync(new Uri(downloadUrl), name); } CommandManager.InvalidateRequerySuggested(); } } } catch (WebException web) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading Resources", web.Message + Environment.NewLine + "Trying to load files already downloaded.")); await LoadFilesAsync(); } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading Resources", ex.Message)); } GC.Collect(); } private async Task DownloadFileAsync2(Uri uri, string name) { try { var file = Path.Combine(Dispatcher.Invoke(() => _tempPath), name); if (File.Exists(file)) File.Delete(file); using (var webClient = new WebClient { Credentials = CredentialCache.DefaultNetworkCredentials }) await webClient.DownloadFileTaskAsync(uri, file); //Saves the template for later, when exporting the translation. if (name.EndsWith("en.xaml")) { using (var sr = new StreamReader(file, Encoding.UTF8)) { _resourceTemplate = await sr.ReadToEndAsync(); } } await using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { var dictionary = await Task.Factory.StartNew(() => (ResourceDictionary)XamlReader.Load(fs, new ParserContext { XmlSpace = "preserve" })); //var dictionary = new ResourceDictionary(); dictionary.Source = await Task.Factory.StartNew(() => new Uri(Path.GetFullPath(file), UriKind.Absolute)); _resourceList.Add(dictionary); if (name.EndsWith("en.xaml")) Application.Current.Resources.MergedDictionaries.Add(dictionary); } } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading File", ex.Message)); } } private async Task DownloadFileAsync(Uri uri, string name) { try { var file = Path.Combine(Dispatcher.Invoke(() => _tempPath), name); if (File.Exists(file)) File.Delete(file); using (var webClient = new WebClient { Credentials = CredentialCache.DefaultNetworkCredentials }) await webClient.DownloadFileTaskAsync(uri, file); //Replaces the special chars. var text = await Task.Factory.StartNew(() => File.ReadAllText(file, Encoding.UTF8).Replace("&#", "&#")); await Task.Factory.StartNew(() => File.WriteAllText(file, text, Encoding.UTF8)); //Saves the template for later, when exporting the translation. if (name.EndsWith("en.xaml")) _resourceTemplate = text; var dictionary = await Task.Factory.StartNew(() => new ResourceDictionary { Source = new Uri(Path.GetFullPath(file), UriKind.Absolute) }); _resourceList.Add(dictionary); //if (name.EndsWith("en.xaml")) // Application.Current.Resources.MergedDictionaries.Add(dictionary); //using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(text))) //{ // var dictionary = (ResourceDictionary)System.Windows.Markup.XamlReader.Load(stream, new ParserContext { XmlSpace = "preserve" }); //} } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Downloading File", ex.Message)); } } private async Task LoadFilesAsync() { try { var files = await Task.Factory.StartNew(() => Directory.EnumerateFiles(_tempPath, "*.xaml")); foreach (var file in files) { //Replaces the special chars. var text = await Task.Factory.StartNew(() => File.ReadAllText(file, Encoding.UTF8).Replace("&#", "&#")); await Task.Factory.StartNew(() => File.WriteAllText(file, text, Encoding.UTF8)); //Saves the template for later, when exporting the translation. if (file.EndsWith("en.xaml")) _resourceTemplate = text; var dictionary = await Task.Factory.StartNew(() => new ResourceDictionary { Source = new Uri(Path.GetFullPath(file), UriKind.Absolute) }); _resourceList.Add(dictionary); } } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Loading Offline File", ex.Message)); } } private void ShowTranslations(string baseCulture, string specificCulture) { //var baseCulture = FromComboBox.SelectionBoxItem as Culture; //var specificCulture = ToComboBox.SelectionBoxItem as Culture; if (baseCulture == null) { _translationList = null; BaseDataGrid.ItemsSource = null; return; } var baseResource = _resourceList.FirstOrDefault(x => x.Source?.OriginalString.EndsWith(baseCulture + ".xaml") ?? false); //var baseResource = Application.Current.Resources.MergedDictionaries.FirstOrDefault(x => x.Source?.OriginalString.EndsWith(baseCulture + ".xaml") ?? false); if (baseResource == null) return; if (specificCulture == null) { _translationList = new ObservableCollection(baseResource.Keys.Cast().Select(y => new Translation { Key = y, BaseText = (string)baseResource[y] }).OrderBy(o => o.Key).ToList()); BaseDataGrid.ItemsSource = _translationList; return; } var specificResource = _resourceList.LastOrDefault(x => x.Source?.OriginalString.EndsWith(specificCulture + ".xaml") ?? false); if (specificResource == null) { _translationList = new ObservableCollection(baseResource.Keys.Cast().Select(y => new Translation { Key = y, BaseText = (string)baseResource[y] }).OrderBy(o => o.Key).ToList()); BaseDataGrid.ItemsSource = _translationList; return; } _translationList = new ObservableCollection(baseResource.Keys.Cast().Select(y => new Translation { Key = y, BaseText = (string)baseResource[y], SpecificText = (string)specificResource[y] }).OrderBy(o => o.Key).ToList()); BaseDataGrid.ItemsSource = _translationList; } private bool ExportTranslation(string path) { try { var lines = _resourceTemplate.Split('\n'); for (var i = 0; i < lines.Length; i++) { var keyIndex = lines[i].IndexOf(":Key=", StringComparison.Ordinal); if (lines[i].TrimStart().StartsWith(""; //Comment the line. else lines[i] = $" {translated.SpecificText}"; } if (File.Exists(path)) File.Delete(path); File.WriteAllText(path, string.Join(Environment.NewLine, lines).Replace("&#", "&#"), Encoding.UTF8); return true; } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Saving Translation", ex.Message)); return false; } } private string CheckSupportedCulture(string cultureName) { //Using HashSet, because we can check if it contains string in O(1) time //Only creating it takes some time, //but it's better than performing Contains multiple times on the list in the loop below var cultureHash = new HashSet(_cultures); if (cultureHash.Contains(cultureName)) return cultureName; var t = CultureInfo.GetCultureInfo(cultureName); while (t != CultureInfo.InvariantCulture) { if (cultureHash.Contains(t.Name)) return t.Name; t = t.Parent; } throw new CultureNotFoundException(); } private async Task> GetProperCulturesAsync() { var allCodes = await Task.Factory.StartNew(() => CultureInfo.GetCultures(CultureTypes.AllCultures).Where(x => !string.IsNullOrEmpty(x.Name)).Select(x => x.Name)); try { var downloadedCodes = GetLanguageCodesOffline(); var properCodes = await Task.Factory.StartNew(() => allCodes.Where(x => downloadedCodes.Contains(x))); return properCodes ?? allCodes; } catch (Exception ex) { Dispatcher.Invoke(() => Dialog.Ok("Translator", "Translator - Getting Language Codes", ex.Message + Environment.NewLine + "Loading all local language codes.")); } GC.Collect(); return allCodes; } private List GetLanguageCodesOffline() { //I'm taking a shortcut in here. return ("af;af-NA;agq;ak;am;ar;ar-AE;ar-BH;ar-DJ;ar-DZ;ar-EG;ar-ER;ar-IL;ar-IQ;ar-JO;ar-KM;ar-KW;ar-LB;ar-LY;ar-MA;ar-MR;ar-OM;ar-PS;ar-QA;ar-SA;ar-SD;ar-SO;" + "ar-SS;ar-SY;ar-TD;ar-TN;ar-YE;as;asa;ast;az;az-Cyrl;bas;be;bem;bez;bg;bm;bn;bn-IN;bo;bo-IN;br;brx;bs;bs-Cyrl;ca;ca-FR;ccp;ce;ceb;cgg;chr;cs;cu;cy;da;" + "dav;de;de-AT;de-CH;de-IT;de-LI;de-LU;dje;dsb;dua;dyo;dz;ebu;ee;ee-TG;el;en;en-001;en-150;en-AE;en-AG;en-AI;en-AT;en-AU;en-BB;en-BE;en-BI;en-BM;en-BS;" + "en-BW;en-BZ;en-CA;en-CC;en-CH;en-CK;en-CM;en-CX;en-DE;en-DK;en-DM;en-ER;en-FI;en-FJ;en-FK;en-GB;en-GD;en-GG;en-GH;en-GI;en-GM;en-GU;en-GY;en-HK;en-IE;" + "en-IL;en-IM;en-IN;en-IO;en-JE;en-JM;en-KE;en-KI;en-KN;en-KY;en-LC;en-LR;en-LS;en-MG;en-MH;en-MO;en-MP;en-MS;en-MT;en-MU;en-MW;en-MY;en-NA;en-NF;en-NG;" + "en-NL;en-NR;en-NU;en-NZ;en-PG;en-PH;en-PK;en-PN;en-PW;en-RW;en-SB;en-SC;en-SD;en-SE;en-SG;en-SH;en-SI;en-SL;en-SS;en-SX;en-SZ;en-TK;en-TO;en-TT;en-TV;" + "en-TZ;en-UG;en-VC;en-VU;en-WS;en-ZA;en-ZM;en-ZW;eo;es;es-419;es-AR;es-BO;es-BR;es-BZ;es-CL;es-CO;es-CR;es-CU;es-DO;es-EC;es-GQ;es-GT;es-HN;es-MX;es-NI;" + "es-PA;es-PE;es-PH;es-PR;es-PY;es-SV;es-US;es-UY;es-VE;et;eu;ewo;fa;ff;ff-Latn-GH;ff-Latn-GM;ff-Latn-GN;ff-Latn-LR;ff-Latn-MR;ff-Latn-NG;ff-Latn-SL;fi;fil;" + "fo;fo-DK;fr;fr-BE;fr-BI;fr-CA;fr-CD;fr-CH;fr-CI;fr-CM;fr-DJ;fr-DZ;fr-GF;fr-GN;fr-HT;fr-KM;fr-LU;fr-MA;fr-MG;fr-ML;fr-MR;fr-MU;fr-RE;fr-RW;fr-SC;fr-SN;fr-SY;" + "fr-TD;fr-TN;fr-VU;fur;fy;ga;gd;gl;gsw;gu;guz;gv;ha;haw;he;hi;hr;hr-BA;hsb;hu;hy;ia;id;ig;ii;is;it;it-CH;ja;jgo;jmc;jv;ka;kab;kam;kde;kea;khq;ki;kk;kkj;kl;kln;" + "km;kn;ko;ko-KP;kok;ks;ksb;ksf;ksh;ku;kw;ky;lag;lb;lg;lkt;ln;ln-AO;lo;lrc;lrc-IQ;lt;lu;luo;luy;lv;mas;mas-TZ;mer;mfe;mg;mgh;mgo;mi;mk;ml;mn;mni;mr;ms;ms-BN;ms-SG;" + "mt;mua;my;mzn;naq;nb;nd;nds;nds-NL;ne;ne-IN;nl;nl-AW;nl-BE;nl-BQ;nl-CW;nl-SR;nl-SX;nmg;nn;nnh;nus;nyn;om;om-KE;or;os;os-RU;pa;pa-Arab;pl;prg;ps;ps-PK;pt;pt-AO;" + "pt-CV;pt-GW;pt-LU;pt-MO;pt-MZ;pt-PT;pt-ST;pt-TL;rm;rn;ro;ro-MD;rof;ru;ru-BY;ru-KG;ru-KZ;ru-MD;ru-UA;rw;rwk;sah;saq;sbp;sd;sd-Deva;se;se-FI;se-SE;seh;ses;sg;shi;" + "shi-Latn;si;sk;sl;smn;sn;so;so-DJ;so-ET;so-KE;sq;sq-MK;sq-XK;sr;sr-Cyrl-BA;sr-Cyrl-ME;sr-Cyrl-XK;sr-Latn;sr-Latn-BA;sr-Latn-ME;sr-Latn-XK;sv;sv-FI;sw;sw-CD;sw-KE;" + "sw-UG;ta;ta-LK;ta-MY;ta-SG;te;teo;teo-KE;tg;th;ti;ti-ER;tk;to;tr;tr-CY;tt;twq;tzm;ug;uk;ur;ur-IN;uz;uz-Arab;uz-Cyrl;vai;vai-Latn;vi;vo;vun;wae;wo;xh;xog;yav;yi;yo;" + "yo-BJ;zgh;zh;zh-Hans-HK;zh-Hans-MO;zh-Hant;zu").Split(';').ToList(); } private async Task> GetLanguageCodesAsync() { var path = await GetLanguageCodesPathAsync(); if (string.IsNullOrEmpty(path)) throw new WebException("Can't get language codes. Path to language codes is null"); var request = (HttpWebRequest)WebRequest.Create(path); request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; var response = (HttpWebResponse)await request.GetResponseAsync(); using (var resultStream = response.GetResponseStream()) { if (resultStream == null) throw new WebException("Empty response from server when getting language codes"); using (var reader = new StreamReader(resultStream)) { var result = await reader.ReadToEndAsync(); var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(result), new System.Xml.XmlDictionaryReaderQuotas()); var json = await Task.Factory.StartNew(() => XElement.Load(jsonReader)); var languages = json.Elements(); return await Task.Factory.StartNew(() => languages.Where(x => x.XPathSelectElement("defs").Value != "0").Select(x => x.XPathSelectElement("lang").Value)); } } } private async Task GetLanguageCodesPathAsync() { var request = (HttpWebRequest)WebRequest.Create("https://datahub.io/core/language-codes/datapackage.json"); request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; var response = (HttpWebResponse)await request.GetResponseAsync(); using (var resultStream = response.GetResponseStream()) { if (resultStream == null) throw new WebException("Empty response from server when getting language codes path"); using (var reader = new StreamReader(resultStream)) { var result = await reader.ReadToEndAsync(); var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(result), new System.Xml.XmlDictionaryReaderQuotas()); var json = await Task.Factory.StartNew(() => XElement.Load(jsonReader)); return await Task.Factory.StartNew(() => json.XPathSelectElement("resources").Elements().First(x => x.XPathSelectElement("name").Value == "ietf-language-tags_json").XPathSelectElement("path").Value); } } } #endregion } internal class Culture { public string Code { get; set; } public string Name { get; set; } public string CodeName => Code.PadRight(3) + " - " + Name; } internal class Translation { public string Key { get; set; } public string BaseText { get; set; } public string SpecificText { get; set; } } ================================================ FILE: Other/Translator/Util/DataGridHelper.cs ================================================ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Media; namespace Translator.Util; public static class DataGridHelper { public static DataGridCell GetCell(DataGrid dg, int row, int column) { var rowContainer = GetRow(dg, row); if (rowContainer != null) { var presenter = VisualHelper.GetVisualChild(rowContainer); // try to get the cell but it may possibly be virtualized var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column); if (cell == null) { // now try to bring into view and retrieve the cell dg.ScrollIntoView(rowContainer, dg.Columns[column]); cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column); } return cell; } return null; } public static DataGridRow GetRow(DataGrid dg, int index) { dg.UpdateLayout(); var row = (DataGridRow)dg.ItemContainerGenerator.ContainerFromIndex(index); if (row == null) { // may be virtualized, bring into view and try again dg.ScrollIntoView(dg.Items[index]); row = (DataGridRow)dg.ItemContainerGenerator.ContainerFromIndex(index); } return row; } public static int GetRowIndex(DataGrid dg, DataGridCellInfo dgci) { if (!dgci.IsValid) return -1; var dgrow = (DataGridRow)dg.ItemContainerGenerator.ContainerFromItem(dgci.Item); return dgrow?.GetIndex() ?? -1; } public static int GetColIndex(DataGridCellInfo dgci) { return dgci.Column.DisplayIndex; } public static DataGridCell FindParentCell(DataGrid grid, DependencyObject child, int i) { var parent = VisualTreeHelper.GetParent(child); var logicalParent = LogicalTreeHelper.GetParent(child); if (logicalParent is DataGridCell) return logicalParent as DataGridCell; if (i > 4 || parent == null || parent is DataGridCell) return parent as DataGridCell; return FindParentCell(grid, parent, i + 1); } public static DataGridCell GetDataGridCell(DataGridCellInfo cellInfo) { if (cellInfo.IsValid == false) return null; var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item); return cellContent?.Parent as DataGridCell; } public static DataGridCell GetDataGridCell(DataGrid dataGrid) { if (dataGrid.CurrentCell.IsValid == false) return null; var cellContent = dataGrid.CurrentCell.Column.GetCellContent(dataGrid.CurrentCell.Item); if (cellContent == null) { return GetCell(dataGrid, GetColIndex(dataGrid.CurrentCell), GetRowIndex(dataGrid, dataGrid.CurrentCell)); } return cellContent.Parent as DataGridCell; } public static void FocusOnFirstCell(this DataGrid dataGrid) { dataGrid.SelectedIndex = 0; //dataGrid.CurrentCell = new DataGridCellInfo(DataGrid.Items[0], DataGrid.Columns[0]); var cell = GetCell(dataGrid, 0, 0); cell?.Focus(); } public static bool Sort(this DataGrid grid, ListSortDirection direction, string property, string second = null) { //If there's already a sort defined in another property. foreach (var column in grid.Columns) { if (column.SortDirection.HasValue) return false; var dataColumn = column as DataGridTextColumn; if (dataColumn == null || dataColumn.Binding == null) continue; var binding = dataColumn.Binding as Binding; if (binding != null && binding.Path != null && binding.Path.Path == property) column.SortDirection = direction; } //Add the new sort description. grid.Items.SortDescriptions.Add(new SortDescription(property, direction)); if (second != null) grid.Items.SortDescriptions.Add(new SortDescription(second, direction)); return true; } public static void ReSort(this DataGrid grid, Dictionary sorted) { if (sorted == null || !sorted.Any()) sorted = grid.Columns.Where(x => x.SortDirection.HasValue) .ToDictionary(w => w.SortMemberPath, w => w.SortDirection.Value); grid.Items.SortDescriptions.Clear(); foreach (var sort in sorted) { #region Search for the column that should be sorted var column = grid.Columns.FirstOrDefault(x => { var dataColumn = x as DataGridTextColumn; if (dataColumn == null || dataColumn.Binding == null) return false; var binding = dataColumn.Binding as Binding; //Only returns true if it's the match. if (binding != null && binding.Path != null && binding.Path.Path == sort.Key) return true; return false; }); #endregion //Displays the sort direction glyph. if (column != null) column.SortDirection = sort.Value; //Add the new sort description. grid.Items.SortDescriptions.Add(new SortDescription(sort.Key, sort.Value)); } } } ================================================ FILE: Other/Translator/Util/LogWriter.cs ================================================ using System; using System.IO; namespace Translator.Util; /// /// Log Writer Class /// public static class LogWriter { /// /// Write to Error Log (Text File). /// /// The Exception to write. /// The name of the error /// Additional information. /// Fallbacks to the Documents folder. public static void Log(Exception ex, string title, object additional = null, bool isFallback = false) { try { #region Output folder var documents = isFallback ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : "."; var folder = Path.Combine(documents, "ScreenToGif", "Logs"); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); #endregion #region Creates the file var date = Path.Combine(folder, DateTime.Now.ToString("yy_MM_dd") + ".txt"); var dateTime = Path.Combine(folder, DateTime.Now.ToString("yy_MM_dd hh_mm_ss_fff") + ".txt"); FileStream fs = null; var inUse = false; try { fs = new FileStream(date, FileMode.OpenOrCreate, FileAccess.ReadWrite); } catch (Exception) { inUse = true; fs = new FileStream(dateTime, FileMode.OpenOrCreate, FileAccess.ReadWrite); } fs.Dispose(); #endregion #region Append the exception information using (var fileStream = new FileStream(inUse ? dateTime : date, FileMode.Append, FileAccess.Write)) { using (var writer = new StreamWriter(fileStream)) { writer.WriteLine($"► Title - {Environment.NewLine}\t{title}"); writer.WriteLine($"▬ Message - {Environment.NewLine}\t{ex.Message}"); writer.WriteLine($"○ Type - {Environment.NewLine}\t{ex.GetType()}"); writer.WriteLine(FormattableString.Invariant($"♦ [Version] Date/Hour - {Environment.NewLine}\t[{App.Version}] {DateTime.Now}")); writer.WriteLine($"▲ Source - {Environment.NewLine}\t{ex.Source}"); writer.WriteLine($"▼ TargetSite - {Environment.NewLine}\t{ex.TargetSite}"); var bad = ex as BadImageFormatException; if (bad != null) writer.WriteLine($"► Fuslog - {Environment.NewLine}\t{bad.FusionLog}"); if (additional != null) writer.WriteLine($"◄ Additional - {Environment.NewLine}\t{additional}"); writer.WriteLine($"♠ StackTrace - {Environment.NewLine}{ex.StackTrace}"); if (ex.InnerException != null) { writer.WriteLine(); writer.WriteLine($"▬▬ Message - {Environment.NewLine}\t{ex.InnerException.Message}"); writer.WriteLine($"○○ Type - {Environment.NewLine}\t{ex.InnerException.GetType()}"); writer.WriteLine($"▲▲ Source - {Environment.NewLine}\t{ex.InnerException.Source}"); writer.WriteLine($"▼▼ TargetSite - {Environment.NewLine}\t{ex.InnerException.TargetSite}"); writer.WriteLine($"♠♠ StackTrace - {Environment.NewLine}{ex.InnerException.StackTrace}"); if (ex.InnerException.InnerException != null) { writer.WriteLine(); writer.WriteLine($"▬▬▬ Message - {Environment.NewLine}\t{ex.InnerException.InnerException.Message}"); writer.WriteLine($"○○○ Type - {Environment.NewLine}\t{ex.InnerException.InnerException.GetType()}"); writer.WriteLine($"▲▲▲ Source - {Environment.NewLine}\t{ex.InnerException.InnerException.Source}"); writer.WriteLine($"▼▼▼ TargetSite - {Environment.NewLine}\t{ex.InnerException.InnerException.TargetSite}"); writer.WriteLine($"♠♠♠ StackTrace - {Environment.NewLine}\t{ex.InnerException.InnerException.StackTrace}"); if (ex.InnerException.InnerException.InnerException != null) { writer.WriteLine(); writer.WriteLine($"▬▬▬▬ Message - {Environment.NewLine}\t{ex.InnerException.InnerException.InnerException.Message}"); writer.WriteLine($"○○○○ Type - {Environment.NewLine}\t{ex.InnerException.InnerException.InnerException.GetType()}"); writer.WriteLine($"▲▲▲▲ Source - {Environment.NewLine}\t{ex.InnerException.InnerException.InnerException.Source}"); writer.WriteLine($"▼▼▼▼ TargetSite - {Environment.NewLine}\t{ex.InnerException.InnerException.InnerException.TargetSite}"); writer.WriteLine($"♠♠♠♠ StackTrace - {Environment.NewLine}\t{ex.InnerException.InnerException.InnerException.StackTrace}"); } } } writer.WriteLine(); writer.WriteLine("----------------------------------"); writer.WriteLine(); } } #endregion } catch (Exception) { //One last trial. if (!isFallback) Log(ex, title, additional, true); } } } ================================================ FILE: Other/Translator/Util/VisualHelper.cs ================================================ using System; using System.IO; using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; namespace Translator.Util; public static class VisualHelper { public static TP GetParent(DependencyObject child, int i) where TP : DependencyObject { var parent = VisualTreeHelper.GetParent(child); var logicalParent = LogicalTreeHelper.GetParent(child); if (logicalParent is TP) return logicalParent as TP; if (i > 4 || parent == null || parent is TP) return parent as TP; return GetParent(parent, i + 1); } public static TP GetParent(DependencyObject child, Type stopWhen) where TP : Visual { var parent = VisualTreeHelper.GetParent(child); var logicalParent = LogicalTreeHelper.GetParent(child); if (logicalParent is TP) return logicalParent as TP; if (parent is TP) return parent as TP; if (parent == null || parent.GetType() == stopWhen) return null; return GetParent(parent, stopWhen); } public static T GetVisualChild(Visual parent) where T : Visual { var child = default(T); var numVisuals = VisualTreeHelper.GetChildrenCount(parent); for (var i = 0; i < numVisuals; i++) { var v = (Visual)VisualTreeHelper.GetChild(parent, i); child = v as T ?? GetVisualChild(v); if (child != null) break; } return child; } public static T DeepCopy(UIElement source) where T : new() { if (source == null) return new T(); var savedObject = System.Windows.Markup.XamlWriter.Save(source); var stringReader = new StringReader(savedObject); var xmlReader = System.Xml.XmlReader.Create(stringReader); return (T)System.Windows.Markup.XamlReader.Load(xmlReader); } public static Storyboard FindStoryboard(this FrameworkElement visual, string key) { var resource = visual.FindResource(key) as Storyboard; if (resource == null) return new Storyboard(); return resource; } } ================================================ FILE: README.md ================================================
Special thanks to:

Warp sponsorship ### [Warp, built for coding with multiple AI agents](https://www.warp.dev/ScreenToGif) [Available for MacOS, Linux, & Windows](https://www.warp.dev/ScreenToGif)

screen recorder

GitHub stars All releases All Chocolatey releases

ScreenToGif 🎬 screentogif.com

This tool allows you to record a selected area of your screen, live feed from your webcam or live drawings from a sketchboard. Afterward, you can edit and save the animation as a gif, apng, video, psd or png image.

download

download Microsoft Store Download from Chocolatey

⚠️ Attention, it requires .NET 9 Desktop Runtime (or above). ⚠️

Latest GitHub release Latest Chocolatey release Documentation WIP Issues Discord

Would you like to help the project?

* PayPal donation: [![PayPal page](https://img.shields.io/badge/donate-Paypal-fd8200.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=JCY2BGLULSWVJ&lc=US&item_name=ScreenToGif&item_number=screentogif¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) * Patreon subscription: [![Patreon subscription](https://img.shields.io/badge/subscribe-Patreon-orange.svg)](https://www.patreon.com/nicke) * Ko-fi donation: [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B7Y5Z9) * Flattr subscription: https://flattr.com/@NickeManarin/domain/screentogif.com * Steam wishlist: [![Steam wishlist](https://img.shields.io/badge/donate-Steam-171a21.svg)](http://steamcommunity.com/id/nickesm/wishlist) * GOG Galaxy wishlist: https://www.gog.com/u/Nickesm/wishlist * Amazon wishlist: https://www.amazon.com/hz/wishlist/ls/2S54SRWY2K8KF?ref_=wl_share * Feedback (reporting bugs, ideas, etc) [![Author's Twitter](https://img.shields.io/badge/Twitter-%40NickeManarin-blue.svg)](https://twitter.com/NickeManarin) * [Anyone can still contribute to the localization of the app/website/installer](https://github.com/NickeManarin/ScreenToGif/blob/master/LOCALIZATION.md) * Create a review. :)

Please, avoid selling this app as yours

I don't care if you copy the source code to use in your project, but please avoid simply changing the name and selling as your work. That's not why I'm sharing the source code, at all.

Screenshots

start up

start up

editor

option

keystrokes

Mentions

Website
Chip
Softpedia
PortableFreeware

The creator also distributes this app via these websites

* [Chocolatey](https://chocolatey.org/packages/screentogif) * [FOSSHUB](https://www.fosshub.com/ScreenToGif.html) * [Microsoft Store](https://www.microsoft.com/en-us/p/screentogif/9n3sqk8pds8g) ================================================ FILE: ScreenToGif/App.xaml ================================================ Segoe UI Segoe UI Semilight Segoe UI Semibold ================================================ FILE: ScreenToGif/App.xaml.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Threading; using Microsoft.Win32; using ScreenToGif.Controls; using ScreenToGif.Domain.Enums; using ScreenToGif.Native.Helpers; using ScreenToGif.Util; using ScreenToGif.Util.Extensions; using ScreenToGif.Util.Helpers; using ScreenToGif.Util.InterProcessChannel; using ScreenToGif.Util.Settings; using ScreenToGif.ViewModel; using ScreenToGif.Windows.Other; namespace ScreenToGif; public partial class App : IDisposable { #region Properties internal static NotifyIcon NotifyIcon { get; private set; } internal static ApplicationViewModel MainViewModel { get; private set; } private Mutex _mutex; private bool _accepted; private readonly List _exceptionList = []; private readonly Lock _lock = new(); #endregion #region Events private void App_Startup(object sender, StartupEventArgs e) { Global.StartupDateTime = DateTime.Now; //Unhandled Exceptions. AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; DllSecurity.HardenDllSearchPath(); //Increases the duration of the tooltip display. ToolTipService.ShowDurationProperty.OverrideMetadata(typeof(DependencyObject), new FrameworkPropertyMetadata(int.MaxValue)); #if !DEBUG BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailure = BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailureOptions.Continue; #endif SetSecurityProtocol(); //Parse arguments. Arguments.Prepare(e.Args); LocalizationHelper.SelectCulture(UserSettings.All.LanguageCode); ThemeHelper.SelectTheme(UserSettings.All.MainTheme); //Listen to changes in theme. SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; #region Download mode if (Arguments.IsInDownloadMode) { var downloader = new Downloader { DownloadMode = Arguments.DownloadMode, DestinationPath = Arguments.DownloadPath }; downloader.ShowDialog(); Environment.Exit(90); return; } #endregion #region Settings persistence mode if (Arguments.IsInSettingsMode) { SettingsPersistenceChannel.RegisterServer(); return; } #endregion #region If set, it allows only one instance per user //The singleton works on a per-user and per-executable mode. //Meaning that a different user and/or a different executable instances can co-exist. //Part of this code won't work on debug mode, since the SetForegroundWindow() needs focus on the foreground window calling the method. if (UserSettings.All.SingleInstance && !Arguments.NewInstance) { try { using (var thisProcess = Process.GetCurrentProcess()) { var user = System.Security.Principal.WindowsIdentity.GetCurrent().User; var name = thisProcess.MainModule?.FileName ?? Assembly.GetEntryAssembly()?.Location ?? "ScreenToGif"; var location = Convert.ToBase64String(Encoding.UTF8.GetBytes(name)); var mutexName = (user?.Value ?? Environment.UserName) + "_" + location; _mutex = new Mutex(true, mutexName, out _accepted); //If the mutex failed to be accepted, it means that another process already opened it. if (!_accepted) { var warning = true; //Switch to the other app (get only one, if multiple available). Use name of assembly. using (var process = Process.GetProcessesByName(thisProcess.ProcessName).FirstOrDefault(f => f.MainWindowHandle != thisProcess.MainWindowHandle)) { if (process != null) { var handles = WindowHelper.GetWindowHandlesFromProcess(process); //Show the window before setting focus. Native.External.User32.ShowWindow(handles.Count > 0 ? handles[0] : process.Handle, Domain.Enums.Native.ShowWindowCommands.Show); //Set user the focus to the window. Native.External.User32.SetForegroundWindow(handles.Count > 0 ? handles[0] : process.Handle); warning = false; InstanceSwitcherChannel.SendMessage(process.Id, e.Args); } } //If no window available (app is in the system tray), display a warning. if (warning) Dialog.Ok(LocalizationHelper.Get("S.Warning.Single.Title"), LocalizationHelper.Get("S.Warning.Single.Header"), LocalizationHelper.Get("S.Warning.Single.Message"), Icons.Info); Environment.Exit(0); return; } //If this is the first instance, register the inter process channel to listen for other instances. InstanceSwitcherChannel.RegisterServer(InstanceSwitch_Received); } } catch (Exception ex) { LogWriter.Log(ex, "Impossible to check if another instance is running"); } } #endregion //Render mode. RenderOptions.ProcessRenderMode = UserSettings.All.DisableHardwareAcceleration ? RenderMode.SoftwareOnly : RenderMode.Default; SetWorkaroundForDispatcher(); #region Tray icon and view model NotifyIcon = (NotifyIcon)FindResource("NotifyIcon"); if (NotifyIcon != null) { NotifyIcon.Visibility = UserSettings.All.ShowNotificationIcon || UserSettings.All.StartMinimized || UserSettings.All.StartUp == 5 ? Visibility.Visible : Visibility.Collapsed; //Replace the old option with the new setting. if (UserSettings.All.StartUp == 5) { UserSettings.All.StartMinimized = true; UserSettings.All.ShowNotificationIcon = true; UserSettings.All.StartUp = 0; } //using (var iconStream = GetResourceStream(new Uri("pack://application:,,,/Resources/Logo.ico"))?.Stream) //{ // if (iconStream != null) // NotifyIcon.Icon = new System.Drawing.Icon(iconStream); //} } MainViewModel = (ApplicationViewModel)FindResource("AppViewModel") ?? new ApplicationViewModel(); RegisterShortcuts(); #endregion //var test = new TestField(); test.ShowDialog(); Environment.Exit(1); return; //var test = new Windows.EditorEx(); test.ShowDialog(); return; //var test = new Windows.NewWebcam(); test.ShowDialog(); return; //var test = Settings.UserSettings.All.StartupTop; #region Tasks Task.Factory.StartNew(MainViewModel.ClearTemporaryFiles, TaskCreationOptions.LongRunning); Task.Factory.StartNew(async () => await MainViewModel.CheckForUpdates(),TaskCreationOptions.LongRunning); Task.Factory.StartNew(MainViewModel.SendFeedback, TaskCreationOptions.LongRunning); #endregion #region Startup if (Arguments.Open) MainViewModel.Open.Execute(Arguments.WindownToOpen, true); else MainViewModel.Open.Execute(UserSettings.All.StartUp); #endregion } internal static void InstanceSwitch_Received(object _, InstanceSwitcherMessage message) { try { var args = message.Args; if (args?.Length > 0) Arguments.Prepare(args); if (Arguments.Open) MainViewModel.Open.Execute(Arguments.WindownToOpen, true); else MainViewModel.Open.Execute(UserSettings.All.StartUp); } catch (Exception e) { LogWriter.Log(e, "Unable to execute arguments from IPC."); } } private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { LogWriter.Log(e.Exception, "On dispatcher unhandled exception - Unknown"); try { ShowException(e.Exception); } catch (Exception ex) { LogWriter.Log(ex, "Error while displaying the error."); //Ignored. } finally { e.Handled = true; } } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is not Exception exception) return; LogWriter.Log(exception, "Current domain unhandled exception - Unknown"); try { ShowException(exception); } catch (Exception) { //Ignored. } } private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) { if (e.Category != UserPreferenceCategory.General) return; ThemeHelper.SelectTheme(UserSettings.All.MainTheme); if (UserSettings.All.GridColorsFollowSystem) { var isSystemUsingDark = ThemeHelper.IsSystemUsingDarkTheme(); UserSettings.All.GridColor1 = isSystemUsingDark ? Constants.DarkEven : Constants.VeryLightEven; UserSettings.All.GridColor2 = isSystemUsingDark ? Constants.DarkOdd : Constants.VeryLightOdd; } } private void App_Exit(object sender, ExitEventArgs e) { SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; try { MutexList.RemoveAll(); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to remove all mutexes of the opened projects."); } try { NotifyIcon?.Dispose(); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to dispose the system tray icon."); } try { EncodingManager.StopAllEncodings(); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to cancel all encodings."); } try { SettingsExtension.ForceSave(); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to save the user settings."); } try { if (_mutex != null && _accepted) { _mutex.ReleaseMutex(); _accepted = false; } } catch (Exception ex) { LogWriter.Log(ex, "Impossible to release the single instance mutex."); } try { HotKeyCollection.Default.Dispose(); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to dispose the hotkeys."); } } #endregion #region Methods private void SetSecurityProtocol() { try { ServicePointManager.Expect100Continue = true; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; } catch (Exception ex) { LogWriter.Log(ex, "Impossible to set the network properties"); } } private void SetWorkaroundForDispatcher() { try { if (UserSettings.All.WorkaroundQuota) BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailure = BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailureOptions.Reset; #if DEBUG PresentationTraceSources.DataBindingSource.Listeners.Add(new ConsoleTraceListener()); PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Warning; BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailure = BaseCompatibilityPreferences.HandleDispatcherRequestProcessingFailureOptions.Throw; #endif } catch (Exception ex) { LogWriter.Log(ex, "Impossible to set the workaround for the quota crash"); } } internal static void RegisterShortcuts() { //TODO: If startup/editor is open and focused, should I let the hotkeys work? //Registers all shortcuts. var screen = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.RecorderModifiers, UserSettings.All.RecorderShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.OpenRecorder.CanExecute(null)) MainViewModel.OpenRecorder.Execute(null); }, true); var webcam = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.WebcamRecorderModifiers, UserSettings.All.WebcamRecorderShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.OpenWebcamRecorder.CanExecute(null)) MainViewModel.OpenWebcamRecorder.Execute(null); }, true); var board = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.BoardRecorderModifiers, UserSettings.All.BoardRecorderShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.OpenBoardRecorder.CanExecute(null)) MainViewModel.OpenBoardRecorder.Execute(null); }, true); var editor = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.EditorModifiers, UserSettings.All.EditorShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.OpenEditor.CanExecute(null)) MainViewModel.OpenEditor.Execute(null); }, true); var options = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.OptionsModifiers, UserSettings.All.OptionsShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.OpenOptions.CanExecute(null)) MainViewModel.OpenOptions.Execute(null); }, true); var exit = HotKeyCollection.Default.TryRegisterHotKey(UserSettings.All.ExitModifiers, UserSettings.All.ExitShortcut, () => { if (!Global.IgnoreHotKeys && MainViewModel.ExitApplication.CanExecute(null)) MainViewModel.ExitApplication.Execute(null); }, true); //Updates the input gesture text of each command. MainViewModel.RecorderGesture = screen ? KeyHelper.GetSelectKeyText(UserSettings.All.RecorderShortcut, UserSettings.All.RecorderModifiers, true, true) : ""; MainViewModel.WebcamRecorderGesture = webcam ? KeyHelper.GetSelectKeyText(UserSettings.All.WebcamRecorderShortcut, UserSettings.All.WebcamRecorderModifiers, true, true) : ""; MainViewModel.BoardRecorderGesture = board ? KeyHelper.GetSelectKeyText(UserSettings.All.BoardRecorderShortcut, UserSettings.All.BoardRecorderModifiers, true, true) : ""; MainViewModel.EditorGesture = editor ? KeyHelper.GetSelectKeyText(UserSettings.All.EditorShortcut, UserSettings.All.EditorModifiers, true, true) : ""; MainViewModel.OptionsGesture = options ? KeyHelper.GetSelectKeyText(UserSettings.All.OptionsShortcut, UserSettings.All.OptionsModifiers, true, true) : ""; MainViewModel.ExitGesture = exit ? KeyHelper.GetSelectKeyText(UserSettings.All.ExitShortcut, UserSettings.All.ExitModifiers, true, true) : ""; } private void ShowException(Exception exception) { lock(_lock) { //Avoid displaying an exception that is already being displayed. if (_exceptionList.Any(a => a.Message == exception.Message)) return; //Adding to the list, so a second exception with the same name won't be displayed. _exceptionList.Add(exception); Current.Dispatcher.Invoke(() => { if (Global.IsHotFix4055002Installed && exception is XamlParseException && exception.InnerException is TargetInvocationException) ExceptionDialog.Ok(exception, "ScreenToGif", "Error while rendering visuals", exception.Message); else ExceptionDialog.Ok(exception, "ScreenToGif", "Unhandled exception", exception.Message); }); //By removing the exception, the same exception can be displayed later. _exceptionList.Remove(exception); } } public void Dispose() { if (_mutex != null && _accepted) { _mutex.ReleaseMutex(); _accepted = false; } _mutex?.Dispose(); } #endregion } ================================================ FILE: ScreenToGif/Capture/BaseCapture.cs ================================================ using System; using System.Collections.Concurrent; using System.Threading.Tasks; using System.Windows; using ScreenToGif.Model; namespace ScreenToGif.Capture; public abstract class BaseCapture : ICapture { private Task _task; #region Properties public bool WasStarted { get; set; } public bool IsAcceptingFrames { get; set; } public int FrameCount { get; set; } public int MinimumDelay { get; set; } public int Left { get; set; } public int Top { get; set; } /// /// The current width of the capture. It can fluctuate, based on the DPI of the current screen. /// public int Width { get; set; } /// /// The current height of the capture. It can fluctuate, based on the DPI of the current screen. /// public int Height { get; set; } /// /// The starting width of the capture. /// public int StartWidth { get; set; } /// /// The starting height of the capture. /// public int StartHeight { get; set; } /// /// The starting scale of the recording. /// public double StartScale { get; set; } /// /// The current scale of the recording. /// public double Scale { get; set; } /// /// The difference in scale from the start frame to the current frame. /// public double ScaleDiff => StartScale / Scale; /// /// The name of the monitor device where the recording is supposed to happen. /// public string DeviceName { get; set; } public ProjectInfo Project { get; set; } public Action OnError { get; set; } protected BlockingCollection BlockingCollection { get; private set; } = new(); #endregion ~BaseCapture() { Dispose(); } public virtual void Start(int delay, int left, int top, int width, int height, double scale, ProjectInfo project) { if (WasStarted) throw new Exception("Screen capture was already started. Stop before trying again."); FrameCount = 0; MinimumDelay = delay; Left = left; Top = top; StartWidth = Width = width; StartHeight = Height = height; StartScale = scale; Scale = scale; Project = project; Project.Width = width; Project.Height = height; Project.Dpi = 96 * scale; BlockingCollection ??= new BlockingCollection(); //Spin up a Task to consume the BlockingCollection. _task = Task.Factory.StartNew(() => { try { while (true) Save(BlockingCollection.Take()); } catch (InvalidOperationException) { //It means that Take() was called on a completed collection. } catch (Exception e) { Application.Current.Dispatcher.Invoke(() => OnError?.Invoke(e)); } }); WasStarted = true; IsAcceptingFrames = true; } public virtual void ResetConfiguration() { } public virtual void Save(FrameInfo info) { } public virtual int Capture(FrameInfo frame) { return 0; } public virtual Task CaptureAsync(FrameInfo frame) { return null; } public virtual int CaptureWithCursor(FrameInfo frame) { return 0; } public virtual Task CaptureWithCursorAsync(FrameInfo frame) { return null; } public virtual int ManualCapture(FrameInfo frame, bool showCursor = false) { return showCursor ? CaptureWithCursor(frame) : Capture(frame); } public virtual Task ManualCaptureAsync(FrameInfo frame, bool showCursor = false) { return showCursor ? CaptureWithCursorAsync(frame) : CaptureAsync(frame); } public virtual async Task Stop() { if (!WasStarted) return; IsAcceptingFrames = false; //Stop the consumer thread. BlockingCollection.CompleteAdding(); await _task; WasStarted = false; } private async Task DisposeInternal() { if (WasStarted) await Stop(); _task?.Dispose(); _task = null; BlockingCollection?.Dispose(); BlockingCollection = null; } public virtual async ValueTask DisposeAsync() { await DisposeInternal(); GC.SuppressFinalize(this); } public void Dispose() { DisposeInternal().Wait(); GC.SuppressFinalize(this); } } ================================================ FILE: ScreenToGif/Capture/CachedCapture.cs ================================================ using System; using System.IO; using System.IO.Compression; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using ScreenToGif.Domain.Enums.Native; using ScreenToGif.Model; using ScreenToGif.Native.External; using ScreenToGif.Native.Structs; using ScreenToGif.Util; using ScreenToGif.Util.Settings; namespace ScreenToGif.Capture; internal class CachedCapture : ImageCapture { #region Variables private FileStream _fileStream; private BufferedStream _bufferedStream; private DeflateStream _compressStream; private BitmapInfoHeader _infoHeader; private long _byteLength; #endregion public override void Start(int delay, int left, int top, int width, int height, double scale, ProjectInfo project) { base.Start(delay, left, top, width, height, scale, project); _infoHeader = new BitmapInfoHeader(); _infoHeader.biSize = (uint)Marshal.SizeOf(_infoHeader); _infoHeader.biBitCount = 24; //Without alpha channel. _infoHeader.biClrUsed = 0; _infoHeader.biClrImportant = 0; _infoHeader.biCompression = 0; _infoHeader.biHeight = -StartHeight; //Negative, so the Y-axis will be positioned correctly. _infoHeader.biWidth = StartWidth; _infoHeader.biPlanes = 1; //This was working with 32 bits: 3L * Width * Height; _byteLength = (StartWidth * _infoHeader.biBitCount + 31) / 32 * 4 * StartHeight; //Due to a strange behavior with the GetDiBits method while the cursor is IBeam, it's best to use 24 bits, to ignore the alpha values. //This capture mode ignores the alpha value. project.BitDepth = 24; _fileStream = new FileStream(project.CachePath, FileMode.Create, FileAccess.Write, FileShare.None); _bufferedStream = new BufferedStream(_fileStream, UserSettings.All.MemoryCacheSize * 1048576); //Each 1 MB has 1_048_576 bytes. _compressStream = new DeflateStream(_bufferedStream, UserSettings.All.CaptureCompression, true); } public override int Capture(FrameInfo frame) { try { //var success = Native.BitBlt(CompatibleDeviceContext, 0, 0, Width, Height, WindowDeviceContext, Left, Top, Native.CopyPixelOperation.SourceCopy | Native.CopyPixelOperation.CaptureBlt); var success = Gdi32.StretchBlt(CompatibleDeviceContext, 0, 0, StartWidth, StartHeight, WindowDeviceContext, Left, Top, Width, Height, CopyPixelOperations.SourceCopy | CopyPixelOperations.CaptureBlt); if (!success) return FrameCount; //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = _byteLength; frame.Data = new byte[_byteLength]; if (Gdi32.GetDIBits(WindowDeviceContext, CompatibleBitmap, 0, (uint)StartHeight, frame.Data, ref _infoHeader, DibColorModes.RgbColors) == 0) frame.FrameSkipped = true; if (IsAcceptingFrames) BlockingCollection.Add(frame); } catch (Exception) { //LogWriter.Log(ex, "Impossible to get screenshot of the screen"); } return FrameCount; } public override int CaptureWithCursor(FrameInfo frame) { try { //var success = Native.BitBlt(CompatibleDeviceContext, 0, 0, Width, Height, WindowDeviceContext, Left, Top, Native.CopyPixelOperation.SourceCopy | Native.CopyPixelOperation.CaptureBlt); var success = Gdi32.StretchBlt(CompatibleDeviceContext, 0, 0, StartWidth, StartHeight, WindowDeviceContext, Left, Top, Width, Height, CopyPixelOperations.SourceCopy | CopyPixelOperations.CaptureBlt); if (!success) return FrameCount; #region Cursor try { var cursorInfo = new CursorInfo(); cursorInfo.cbSize = Marshal.SizeOf(cursorInfo); if (User32.GetCursorInfo(out cursorInfo)) { if (cursorInfo.flags == Native.Constants.CursorShowing) { var hicon = User32.CopyIcon(cursorInfo.hCursor); if (hicon != IntPtr.Zero) { if (User32.GetIconInfo(hicon, out var iconInfo)) { frame.CursorX = cursorInfo.ptScreenPos.X - Left; frame.CursorY = cursorInfo.ptScreenPos.Y - Top; //If the cursor rate needs to be precisely captured. //https://source.winehq.org/source/dlls/user32/cursoricon.c#2325 //int rate = 0, num = 0; //var ok1 = Native.GetCursorFrameInfo(cursorInfo.hCursor, IntPtr.Zero, 17, ref rate, ref num); //CursorStep var ok = User32.DrawIconEx(CompatibleDeviceContext, frame.CursorX - iconInfo.xHotspot, frame.CursorY - iconInfo.yHotspot, cursorInfo.hCursor, 0, 0, CursorStep, IntPtr.Zero, 0x0003); if (!ok) { CursorStep = 0; User32.DrawIconEx(CompatibleDeviceContext, frame.CursorX - iconInfo.xHotspot, frame.CursorY - iconInfo.yHotspot, cursorInfo.hCursor, 0, 0, CursorStep, IntPtr.Zero, 0x0003); } else CursorStep++; //Set to fix all alpha bits back to 255. //frame.RemoveAnyTransparency = iconInfo.hbmMask != IntPtr.Zero; } Gdi32.DeleteObject(iconInfo.hbmColor); Gdi32.DeleteObject(iconInfo.hbmMask); } User32.DestroyIcon(hicon); } Gdi32.DeleteObject(cursorInfo.hCursor); } } catch (Exception e) { //LogWriter.Log(e, "Impossible to get the cursor"); } #endregion //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = _byteLength; frame.Data = new byte[_byteLength]; if (Gdi32.GetDIBits(WindowDeviceContext, CompatibleBitmap, 0, (uint)StartHeight, frame.Data, ref _infoHeader, DibColorModes.RgbColors) == 0) frame.FrameSkipped = true; if (IsAcceptingFrames) BlockingCollection.Add(frame); } catch (Exception e) { //LogWriter.Log(ex, "Impossible to get the screenshot of the screen"); } return FrameCount; } public override void Save(FrameInfo info) { if (UserSettings.All.PreventBlackFrames && info.Data != null && !info.FrameSkipped && info.Data[0] == 0) { if (!info.Data.Any(a => a > 0)) info.FrameSkipped = true; } //If the frame skipped, just increase the delay to the previous frame. if (info.FrameSkipped || info.Data == null) { info.Data = null; //Pass the duration to the previous frame, if any. if (Project.Frames.Count > 0) Project.Frames[Project.Frames.Count - 1].Delay += info.Delay; return; } _compressStream.WriteBytes(info.Data); info.Data = null; Project.Frames.Add(info); } public override async Task Stop() { if (!WasStarted) return; //Stop the recording first. await base.Stop(); //Then close the streams. //_compressStream.Flush(); await _compressStream.DisposeAsync(); await _bufferedStream.FlushAsync(); await _fileStream.FlushAsync(); await _bufferedStream.DisposeAsync(); await _fileStream.DisposeAsync(); } [Obsolete("Only for test")] public void Other() { var hDc = User32.GetWindowDC(IntPtr.Zero); var hMemDc = Gdi32.CreateCompatibleDC(hDc); var bi = new BitmapInfoHeader(); bi.biSize = (uint)Marshal.SizeOf(bi); bi.biBitCount = 24; //Creating RGB bitmap. The following three members don't matter bi.biClrUsed = 0; bi.biClrImportant = 0; bi.biCompression = 0; bi.biHeight = Height; bi.biWidth = Width; bi.biPlanes = 1; var cb = (int)(bi.biHeight * bi.biWidth * bi.biBitCount / 8); //8 is bits per byte. bi.biSizeImage = (uint)(((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8) * bi.biHeight); //bi.biXPelsPerMeter = XPelsPerMeter; //bi.biYPelsPerMeter = YPelsPerMeter; bi.biXPelsPerMeter = 96; bi.biYPelsPerMeter = 96; var pBits = IntPtr.Zero; //Allocate memory for bitmap bits var pBI = Kernel32.LocalAlloc((uint)LocalMemoryFlags.LPTR, new UIntPtr(bi.biSize)); // Not sure if this needed - simply trying to keep marshaller happy Marshal.StructureToPtr(bi, pBI, false); //This will return IntPtr to actual DIB bits in pBits var hBmp = Gdi32.CreateDIBSection(hDc, ref pBI, 0, out pBits, IntPtr.Zero, 0); //Marshall back - now we have BitmapInfoHeader correctly filled in Marshal.PtrToStructure(pBI, bi); var biNew = (BitmapInfoHeader)Marshal.PtrToStructure(pBI, typeof(BitmapInfoHeader)); //Usual stuff var hOldBitmap = Gdi32.SelectObject(hMemDc, hBmp); //Grab bitmap var nRet = Gdi32.BitBlt(hMemDc, 0, 0, bi.biWidth, bi.biHeight, hDc, Left, Top, CopyPixelOperations.SourceCopy | CopyPixelOperations.CaptureBlt); // Allocate memory for a copy of bitmap bits var realBits = new byte[cb]; // And grab bits from DIBSestion data Marshal.Copy(pBits, realBits, 0, cb); //This simply creates valid bitmap file header, so it can be saved to disk var bfh = new BitmapFileHeader(); bfh.bfSize = (uint)cb + 0x36; // Size of header + size of Native.BitmapInfoHeader size of bitmap bits bfh.bfType = 0x4d42; //BM bfh.bfOffBits = 0x36; // var hdrSize = 14; var header = new byte[hdrSize]; BitConverter.GetBytes(bfh.bfType).CopyTo(header, 0); BitConverter.GetBytes(bfh.bfSize).CopyTo(header, 2); BitConverter.GetBytes(bfh.bfOffBits).CopyTo(header, 10); //Allocate enough memory for complete bitmap file var data = new byte[cb + bfh.bfOffBits]; //BITMAPFILEHEADER header.CopyTo(data, 0); //BitmapInfoHeader header = new byte[Marshal.SizeOf(bi)]; var pHeader = Kernel32.LocalAlloc((uint)LocalMemoryFlags.LPTR, new UIntPtr((uint)Marshal.SizeOf(bi))); Marshal.StructureToPtr(biNew, pHeader, false); Marshal.Copy(pHeader, header, 0, Marshal.SizeOf(bi)); Kernel32.LocalFree(pHeader); header.CopyTo(data, hdrSize); //Bitmap bits realBits.CopyTo(data, (int)bfh.bfOffBits); //Native.SelectObject(_compatibleDeviceContext, _oldBitmap); //Native.DeleteObject(_compatibleBitmap); //Native.DeleteDC(_compatibleDeviceContext); //Native.ReleaseDC(_desktopWindow, _windowDeviceContext); Gdi32.SelectObject(hMemDc, hOldBitmap); Gdi32.DeleteObject(hBmp); Gdi32.DeleteDC(hMemDc); User32.ReleaseDC(IntPtr.Zero, hDc); } } ================================================ FILE: ScreenToGif/Capture/DirectCachedCapture.cs ================================================ using System; using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using ScreenToGif.Model; using ScreenToGif.Util; using ScreenToGif.Util.Settings; using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Mathematics.Interop; using MapFlags = SharpDX.Direct3D11.MapFlags; namespace ScreenToGif.Capture; /// /// Frame capture using the DesktopDuplication API and memory cache. /// Adapted from: /// https://github.com/ajorkowski/VirtualSpace /// https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/DXGIDesktopDuplication /// /// How to debug: /// https://walbourn.github.io/dxgi-debug-device/ /// https://walbourn.github.io/direct3d-sdk-debug-layer-tricks/ /// https://devblogs.microsoft.com/cppblog/visual-studio-2015-and-graphics-tools-for-windows-10/ /// internal class DirectCachedCapture : DirectImageCapture { #region Variables private FileStream _fileStream; private BufferedStream _bufferedStream; private DeflateStream _compressStream; #endregion public override void Start(int delay, int left, int top, int width, int height, double dpi, ProjectInfo project) { base.Start(delay, left, top, width, height, dpi, project); _fileStream = new FileStream(project.CachePath, FileMode.Create, FileAccess.Write, FileShare.None); _bufferedStream = new BufferedStream(_fileStream, UserSettings.All.MemoryCacheSize * 1048576); //Each 1 MB has 1_048_576 bytes. _compressStream = new DeflateStream(_bufferedStream, UserSettings.All.CaptureCompression, true); } public override int Capture(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); if (FrameCount == 0 && (res.Failure || resource == null)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0) { //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the screen area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left - OffsetLeft); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width - OffsetLeft); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top - OffsetTop); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), StagingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), StagingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } #endregion } } #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource?.Dispose(); return FrameCount; } //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } if (IsAcceptingFrames) BlockingCollection.Add(frame); #endregion Device.ImmediateContext?.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; if (IsAcceptingFrames) Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override int CaptureWithCursor(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if (FrameCount == 0 && info.LastMouseUpdateTime == 0 && (res.Failure || resource == null)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (FrameCount == 0 && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime > 0) { //Sometimes, the first frame has cursor info, but no screen changes. GetCursor(null, info, frame); resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0) { //Copies the screen data into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the screen area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left - OffsetLeft); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width - OffsetLeft); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top - OffsetTop); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), BackingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); //int left, right, top, bottom; //switch (DisplayRotation) //{ // case DisplayModeRotation.Rotate90: // { // //TODO: // left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); // right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); // top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); // bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); // //left = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); // //right = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); // //top = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); // //bottom = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); // break; // } // case DisplayModeRotation.Rotate180: // { // //TODO: // left = Math.Max(dirtyRectangles[dirtyIndex].Top + OffsetTop, Top); // right = Math.Min(dirtyRectangles[dirtyIndex].Bottom + OffsetTop, Top + Height); // top = Math.Min(dirtyRectangles[dirtyIndex].Right + OffsetLeft, Left + Width); // bottom = Math.Max(dirtyRectangles[dirtyIndex].Left + OffsetLeft, Left); // break; // } // default: // { // //In this context, the screen positions are relative to the current screen, not to the whole set of screens (virtual space). // left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); // right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); // top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); // bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); // break; // } //} //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), BackingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } #endregion } } if (info.TotalMetadataBufferSize > 0 || info.LastMouseUpdateTime > 0) { //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. GetCursor(StagingTexture, info, frame); } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource?.Dispose(); return FrameCount; } //Sets the frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } if (IsAcceptingFrames) BlockingCollection.Add(frame); #endregion Device.ImmediateContext?.UnmapSubresource(StagingTexture, 0); stream.Dispose(); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; if (IsAcceptingFrames) Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override int ManualCapture(FrameInfo frame, bool showCursor = false) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(1000, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if (res.Failure || resource == null || (!showCursor && info.AccumulatedFrames == 0) || (showCursor && info.AccumulatedFrames == 0 && info.LastMouseUpdateTime <= LastProcessTime)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (showCursor && info.AccumulatedFrames == 0 && info.LastMouseUpdateTime > LastProcessTime) { //Gets the cursor shape if the screen hasn't changed in between, so the cursor will be available for the next frame. GetCursor(null, info, frame); resource.Dispose(); return FrameCount; //TODO: if only the mouse changed, but there's no frame accumulated, but there's data in the texture from the previous frame, I need to merge with the cursor and add to the list. } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { if (showCursor) { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), BackingTexture, 0); //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. GetCursor(StagingTexture, info, frame); } else { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), StagingTexture, 0); } } //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource.Dispose(); return FrameCount; } #region Get image data //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream.Dispose(); resource.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; if (IsAcceptingFrames) Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override void Save(FrameInfo info) { System.Diagnostics.Debug.WriteLine("Length:" + info.Data.Length + " " + _fileStream.Length); _compressStream.WriteBytes(info.Data); _compressStream.Flush(); info.Data = null; Project.Frames.Add(info); } public override async Task Stop() { if (!WasStarted) return; //Stop the recording first. await base.Stop(); //Then close the streams. //await _compressStream.FlushAsync(); await _compressStream.DisposeAsync(); await _bufferedStream.FlushAsync(); await _fileStream.FlushAsync(); await _bufferedStream.DisposeAsync(); await _fileStream.DisposeAsync(); } public int Capture2(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Somehow, it was not possible to retrieve the resource or any frame. if (res.Failure || resource == null || info.AccumulatedFrames == 0) { resource?.Dispose(); return FrameCount; } //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), StagingTexture, 0); } //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource.Dispose(); return FrameCount; } #region Get image data //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public int CaptureWithCursor2(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if (res.Failure || resource == null || (info.AccumulatedFrames == 0 && info.LastMouseUpdateTime <= LastProcessTime)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (info.AccumulatedFrames == 0 && info.LastMouseUpdateTime > LastProcessTime) { //Gets the cursor shape if the screen hasn't changed in between, so the cursor will be available for the next frame. GetCursor(null, info, frame); resource.Dispose(); return FrameCount; //TODO: if only the mouse changed, but there's no frame accumulated, but there's data in the texture from the previous frame, I need to merge with the cursor and add to the list. } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), BackingTexture, 0); //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. GetCursor(StagingTexture, info, frame); } //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource.Dispose(); return FrameCount; } #region Get image data //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream.Dispose(); resource.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } } ================================================ FILE: ScreenToGif/Capture/DirectChangedCachedCapture.cs ================================================ using System; using System.Runtime.InteropServices; using System.Windows; using ScreenToGif.Model; using ScreenToGif.Util; using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Mathematics.Interop; using MapFlags = SharpDX.Direct3D11.MapFlags; namespace ScreenToGif.Capture; internal class DirectChangedCachedCapture : DirectCachedCapture { public override int Capture(FrameInfo frame) { var res = new Result(-1); var wasCaptured = false; try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); if (res.Failure || resource == null || info.TotalMetadataBufferSize == 0) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } #region Process changes //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the scree area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), StagingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), StagingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion if (!wasCaptured) { //Nothing was changed within the capture region, so ignore this frame. resource.Dispose(); return FrameCount; } } #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource.Dispose(); return FrameCount; } //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override int CaptureWithCursor(FrameInfo frame) { var res = new Result(-1); var wasCaptured = false; try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if ((res.Failure || resource == null) && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime == 0) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (FrameCount == 0 && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime > 0) { //Sometimes, the first frame has cursor info, but no screen changes. GetCursor(null, info, frame); resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0 && resource != null) { //Copies the screen data into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the scree area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), BackingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), BackingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion } } //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. if (!GetCursor(StagingTexture, info, frame) && !wasCaptured) { //Nothing was changed within the capture region, so ignore this frame. resource?.Dispose(); return FrameCount; } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None, out var stream); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream?.Dispose(); resource?.Dispose(); return FrameCount; } //Sets the frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.DataLength = stream.Length; frame.Data = new byte[stream.Length]; //BGRA32 is 4 bytes. for (var height = 0; height < Height; height++) { stream.Position = height * data.RowPitch; Marshal.Copy(new IntPtr(stream.DataPointer.ToInt64() + height * data.RowPitch), frame.Data, height * Width * 4, Width * 4); } BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); stream.Dispose(); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } } ================================================ FILE: ScreenToGif/Capture/DirectChangedImageCapture.cs ================================================ using System; using System.Drawing.Imaging; using System.Runtime.InteropServices; using ScreenToGif.Model; using ScreenToGif.Util; using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Mathematics.Interop; using MapFlags = SharpDX.Direct3D11.MapFlags; namespace ScreenToGif.Capture; internal class DirectChangedImageCapture : DirectImageCapture { public override int Capture(FrameInfo frame) { var res = new Result(-1); var wasCaptured = false; try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); if (res.Failure || resource == null || info.TotalMetadataBufferSize == 0) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } #region Process changes //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the scree area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), StagingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), StagingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion if (!wasCaptured) { //Nothing was changed within the capture region, so ignore this frame. resource.Dispose(); return FrameCount; } } #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb); var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height); //Copy pixels from screen capture Texture to the GDI bitmap. var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); var sourcePtr = data.DataPointer; var destPtr = mapDest.Scan0; for (var y = 0; y < Height; y++) { //Copy a single line. Utilities.CopyMemory(destPtr, sourcePtr, Width * 4); //Advance pointers. sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch); destPtr = IntPtr.Add(destPtr, mapDest.Stride); } //Release source and dest locks. bitmap.UnlockBits(mapDest); //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = bitmap; BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); OnError.Invoke(ex); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override int CaptureWithCursor(FrameInfo frame) { var res = new Result(-1); var wasCaptured = false; try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if ((res.Failure || resource == null) && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime == 0) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (FrameCount == 0 && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime > 0) { //Sometimes, the first frame has cursor info, but no screen changes. GetCursor(null, info, frame); resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0 && resource != null) { //Copies the screen data into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the scree area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), BackingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), BackingTexture, 0, left - Left, top - Top); wasCaptured = true; } } #endregion } } //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. if (!GetCursor(StagingTexture, info, frame) && !wasCaptured) { //Nothing was changed within the capture region, so ignore this frame. resource?.Dispose(); return FrameCount; } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); #endregion #region Gets the image data //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb); var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height); //Copy pixels from screen capture Texture to the GDI bitmap. var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); var sourcePtr = data.DataPointer; var destPtr = mapDest.Scan0; for (var y = 0; y < Height; y++) { //Copy a single line. Utilities.CopyMemory(destPtr, sourcePtr, Width * 4); //Advance pointers. sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch); destPtr = IntPtr.Add(destPtr, mapDest.Stride); } //Releases the source and dest locks. bitmap.UnlockBits(mapDest); //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = bitmap; BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; OnError.Invoke(ex); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } } ================================================ FILE: ScreenToGif/Capture/DirectImageCapture.cs ================================================ using System; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using ScreenToGif.Domain.Enums.Native; using ScreenToGif.Domain.Exceptions; using ScreenToGif.Model; using ScreenToGif.Native.External; using ScreenToGif.Native.Structs; using ScreenToGif.Util; using SharpDX; using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Mathematics.Interop; using Device = SharpDX.Direct3D11.Device; using MapFlags = SharpDX.Direct3D11.MapFlags; namespace ScreenToGif.Capture; /// /// Frame capture using the DesktopDuplication API. /// Adapted from: /// https://github.com/ajorkowski/VirtualSpace /// https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/DXGIDesktopDuplication /// /// How to debug: /// https://walbourn.github.io/dxgi-debug-device/ /// https://walbourn.github.io/direct3d-sdk-debug-layer-tricks/ /// https://devblogs.microsoft.com/cppblog/visual-studio-2015-and-graphics-tools-for-windows-10/ /// internal class DirectImageCapture : BaseCapture { #region Variables /// /// The current device being duplicated. /// protected internal Device Device; /// /// The desktop duplication interface. /// protected internal OutputDuplication DuplicatedOutput; /// /// The rotation of the screen. /// protected internal DisplayModeRotation DisplayRotation; /// /// The texture used to copy the pixel data from the desktop to the destination image. /// protected internal Texture2D StagingTexture; /// /// The texture used exclusively to be a backing texture when capturing the cursor shape. /// This texture will always hold only the desktop texture, without the cursor. /// protected internal Texture2D BackingTexture; /// /// The texture used exclusively to be a backing texture when capturing screens which are rotated. /// protected internal Texture2D TransformTexture; /// /// Texture used to merge the cursor with the background image (desktop). /// protected internal Texture2D CursorStagingTexture; /// /// The buffer that holds all pixel data of the cursor. /// protected internal byte[] CursorShapeBuffer; /// /// The details of the cursor. /// protected internal OutputDuplicatePointerShapeInformation CursorShapeInfo; /// /// The previous position of the mouse cursor. /// protected internal OutputDuplicatePointerPosition PreviousPosition; /// /// The latest time in which a frame or metadata was captured. /// protected internal long LastProcessTime = 0; protected internal int OffsetLeft { get; set; } protected internal int OffsetTop { get; set; } protected internal int TrueLeft => Left + OffsetLeft; protected internal int TrueRight => Left + OffsetLeft + Width; protected internal int TrueTop => Top + OffsetTop; protected internal int TrueBottom => Top + OffsetTop + Height; /// /// Flag that holds the information whether the previous capture had a major crash. /// protected internal bool MajorCrashHappened = false; #endregion public override void Start(int delay, int left, int top, int width, int height, double dpi, ProjectInfo project) { base.Start(delay, left, top, width, height, dpi, project); //Only set as Started after actually finishing starting. WasStarted = false; Initialize(); WasStarted = true; } public override void ResetConfiguration() { DisposeInternal(); Initialize(); } internal void Initialize() { MajorCrashHappened = false; #if DEBUG Device = new Device(DriverType.Hardware, DeviceCreationFlags.Debug); var debug = SharpDX.DXGI.InfoQueue.TryCreate(); debug?.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Corruption, true); debug?.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Error, true); debug?.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Warning, true); var debug2 = DXGIDebug.TryCreate(); debug2?.ReportLiveObjects(DebugId.Dx, DebugRloFlags.Summary | DebugRloFlags.Detail); #else Device = new Device(DriverType.Hardware, DeviceCreationFlags.VideoSupport); #endif using (var multiThread = Device.QueryInterface()) multiThread.SetMultithreadProtected(true); //Texture used to copy contents from the GPU to be accessible by the CPU. StagingTexture = new Texture2D(Device, new Texture2DDescription { ArraySize = 1, BindFlags = BindFlags.None, CpuAccessFlags = CpuAccessFlags.Read, Format = Format.B8G8R8A8_UNorm, Width = Width, Height = Height, OptionFlags = ResourceOptionFlags.None, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Staging }); //Texture that is used to receive the pixel data from the GPU. BackingTexture = new Texture2D(Device, new Texture2DDescription { ArraySize = 1, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource, CpuAccessFlags = CpuAccessFlags.None, Format = Format.B8G8R8A8_UNorm, Width = Width, Height = Height, OptionFlags = ResourceOptionFlags.None, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Default }); using (var factory = new Factory1()) { //Get the Output1 based on the current capture region position. using (var output1 = GetOutput(factory)) { try { //Make sure to run with the integrated graphics adapter if using a Microsoft hybrid system. https://stackoverflow.com/a/54196789/1735672 DuplicatedOutput = output1.DuplicateOutput(Device); } catch (SharpDXException e) when (e.Descriptor == SharpDX.DXGI.ResultCode.NotCurrentlyAvailable) { throw new Exception("Too many applications using the Desktop Duplication API. Please close one of the applications and try again.", e); } catch (SharpDXException e) when (e.Descriptor == SharpDX.DXGI.ResultCode.Unsupported) { throw new GraphicsConfigurationException("The Desktop Duplication API is not supported on this computer.", e); } catch (SharpDXException e) when (e.Descriptor == SharpDX.DXGI.ResultCode.InvalidCall) { throw new GraphicsConfigurationException("The Desktop Duplication API is not supported on this screen.", e); } catch (SharpDXException e) when (e.Descriptor.NativeApiCode == "E_INVALIDARG") { throw new GraphicsConfigurationException("Looks like that the Desktop Duplication API is not supported on this screen.", e); } } } } /// /// Get the correct Output1 based on region to be captured. /// private Output1 GetOutput(Factory1 factory) { try { //Gets the output with the bigger area being intersected. var output = factory.Adapters1.SelectMany(s => s.Outputs).FirstOrDefault(f => f.Description.DeviceName == DeviceName) ?? factory.Adapters1.SelectMany(s => s.Outputs).OrderByDescending(f => { var x = Math.Max(Left, f.Description.DesktopBounds.Left); var num1 = Math.Min(Left + Width, f.Description.DesktopBounds.Right); var y = Math.Max(Top, f.Description.DesktopBounds.Top); var num2 = Math.Min(Top + Height, f.Description.DesktopBounds.Bottom); if (num1 >= x && num2 >= y) return num1 - x + num2 - y; return 0; }).FirstOrDefault(); if (output == null) throw new Exception($"Could not find a proper output device for the area of L: {Left}, T: {Top}, Width: {Width}, Height: {Height}."); //Position adjustments, so the correct region is captured. OffsetLeft = output.Description.DesktopBounds.Left; OffsetTop = output.Description.DesktopBounds.Top; DisplayRotation = output.Description.Rotation; if (DisplayRotation != DisplayModeRotation.Identity) { //Texture that is used to receive the pixel data from the GPU. TransformTexture = new Texture2D(Device, new Texture2DDescription { ArraySize = 1, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource, CpuAccessFlags = CpuAccessFlags.None, Format = Format.B8G8R8A8_UNorm, Width = Height, Height = Width, OptionFlags = ResourceOptionFlags.None, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Default }); } //Create textures in here, after detecting the orientation? return output.QueryInterface(); } catch (SharpDXException ex) { throw new Exception("Could not find the specified output device.", ex); } } public override int Capture(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); if (FrameCount == 0 && (res.Failure || resource == null)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0) { //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the screen area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left - OffsetLeft); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width - OffsetLeft); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top - OffsetTop); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), StagingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), StagingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } #endregion } } #endregion #region Gets the image data //Gets the staging texture as a stream. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb); var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height); //Copy pixels from screen capture Texture to the GDI bitmap. var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); var sourcePtr = data.DataPointer; var destPtr = mapDest.Scan0; for (var y = 0; y < Height; y++) { //Copy a single line. Utilities.CopyMemory(destPtr, sourcePtr, Width * 4); //Advance pointers. sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch); destPtr = IntPtr.Add(destPtr, mapDest.Stride); } //Release source and dest locks. bitmap.UnlockBits(mapDest); //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = bitmap; if (IsAcceptingFrames) BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; if (IsAcceptingFrames) Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override async Task CaptureAsync(FrameInfo frame) { return await Task.Factory.StartNew(() => Capture(frame)); } public override int CaptureWithCursor(FrameInfo frame) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if (FrameCount == 0 && info.LastMouseUpdateTime == 0 && (res.Failure || resource == null)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. resource?.Dispose(); return FrameCount; } else if (FrameCount == 0 && info.TotalMetadataBufferSize == 0 && info.LastMouseUpdateTime > 0) { //Sometimes, the first frame has cursor info, but no screen changes. GetCursor(null, info, frame); resource?.Dispose(); return FrameCount; } #region Process changes //Something on screen was moved or changed. if (info.TotalMetadataBufferSize > 0) { //Copies the screen data into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { #region Moved rectangles var movedRectangles = new OutputDuplicateMoveRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameMoveRects(movedRectangles.Length, movedRectangles, out var movedRegionsLength); for (var movedIndex = 0; movedIndex < movedRegionsLength / Marshal.SizeOf(typeof(OutputDuplicateMoveRectangle)); movedIndex++) { //Crop the destination rectangle to the screen area rectangle. var left = Math.Max(movedRectangles[movedIndex].DestinationRect.Left, Left - OffsetLeft); var right = Math.Min(movedRectangles[movedIndex].DestinationRect.Right, Left + Width - OffsetLeft); var top = Math.Max(movedRectangles[movedIndex].DestinationRect.Top, Top - OffsetTop); var bottom = Math.Min(movedRectangles[movedIndex].DestinationRect.Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) { //Limit the source rectangle to the available size within the destination rectangle. var sourceWidth = movedRectangles[movedIndex].SourcePoint.X + (right - left); var sourceHeight = movedRectangles[movedIndex].SourcePoint.Y + (bottom - top); Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(movedRectangles[movedIndex].SourcePoint.X, movedRectangles[movedIndex].SourcePoint.Y, 0, sourceWidth, sourceHeight, 1), BackingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } } #endregion #region Dirty rectangles var dirtyRectangles = new RawRectangle[info.TotalMetadataBufferSize]; DuplicatedOutput.GetFrameDirtyRects(dirtyRectangles.Length, dirtyRectangles, out var dirtyRegionsLength); for (var dirtyIndex = 0; dirtyIndex < dirtyRegionsLength / Marshal.SizeOf(typeof(RawRectangle)); dirtyIndex++) { //Crop screen positions and size to frame sizes. var left = Math.Max(dirtyRectangles[dirtyIndex].Left, Left - OffsetLeft); var right = Math.Min(dirtyRectangles[dirtyIndex].Right, Left + Width - OffsetLeft); var top = Math.Max(dirtyRectangles[dirtyIndex].Top, Top - OffsetTop); var bottom = Math.Min(dirtyRectangles[dirtyIndex].Bottom, Top + Height - OffsetTop); //Copies from the screen texture only the area which the user wants to capture. if (right > left && bottom > top) Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(left, top, 0, right, bottom, 1), BackingTexture, 0, left - (Left - OffsetLeft), top - (Top - OffsetTop)); } #endregion } } if (info.TotalMetadataBufferSize > 0 || info.LastMouseUpdateTime > 0) { //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. GetCursor(StagingTexture, info, frame); } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); #endregion #region Gets the image data //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None); if (data.IsEmpty) { Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb); var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height); //Copy pixels from screen capture Texture to the GDI bitmap. var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); var sourcePtr = data.DataPointer; var destPtr = mapDest.Scan0; for (var y = 0; y < Height; y++) { //Copy a single line. Utilities.CopyMemory(destPtr, sourcePtr, Width * 4); //Advance pointers. sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch); destPtr = IntPtr.Add(destPtr, mapDest.Stride); } //Releases the source and dest locks. bitmap.UnlockBits(mapDest); //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = bitmap; if (IsAcceptingFrames) BlockingCollection.Add(frame); #endregion Device.ImmediateContext?.UnmapSubresource(StagingTexture, 0); resource?.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; if (IsAcceptingFrames) Application.Current.Dispatcher.Invoke(() => OnError.Invoke(ex)); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override async Task CaptureWithCursorAsync(FrameInfo frame) { return await Task.Factory.StartNew(() => CaptureWithCursor(frame)); } public override int ManualCapture(FrameInfo frame, bool showCursor = false) { var res = new Result(-1); try { //Try to get the duplicated output frame within given time. res = DuplicatedOutput.TryAcquireNextFrame(1000, out var info, out var resource); //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured. if (res.Failure || resource == null || (!showCursor && info.AccumulatedFrames == 0) || (showCursor && info.AccumulatedFrames == 0 && info.LastMouseUpdateTime <= LastProcessTime)) { //Somehow, it was not possible to retrieve the resource, frame or metadata. //frame.WasDropped = true; //BlockingCollection.Add(frame); resource?.Dispose(); return FrameCount; } else if (showCursor && info.AccumulatedFrames == 0 && info.LastMouseUpdateTime > LastProcessTime) { //Gets the cursor shape if the screen hasn't changed in between, so the cursor will be available for the next frame. GetCursor(null, info, frame); resource.Dispose(); return FrameCount; } //Saves the most recent capture time. LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime); //Copy resource into memory that can be accessed by the CPU. using (var screenTexture = resource.QueryInterface()) { if (showCursor) { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), BackingTexture, 0); //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it. Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture); //Gets the cursor image and merges with the staging texture. GetCursor(StagingTexture, info, frame); } else { //Copies from the screen texture only the area which the user wants to capture. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), StagingTexture, 0); } } //Get the desktop capture texture. var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None); if (data.IsEmpty) { //frame.WasDropped = true; //BlockingCollection.Add(frame); Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource.Dispose(); return FrameCount; } #region Get image data var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb); var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height); //Copy pixels from screen capture Texture to the GDI bitmap. var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat); var sourcePtr = data.DataPointer; var destPtr = mapDest.Scan0; for (var y = 0; y < Height; y++) { //Copy a single line. Utilities.CopyMemory(destPtr, sourcePtr, Width * 4); //Advance pointers. sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch); destPtr = IntPtr.Add(destPtr, mapDest.Stride); } //Release source and dest locks. bitmap.UnlockBits(mapDest); //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = bitmap; BlockingCollection.Add(frame); #endregion Device.ImmediateContext.UnmapSubresource(StagingTexture, 0); resource.Dispose(); return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) { return FrameCount; } catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code) { //When the device gets lost or reset, the resources should be instantiated again. DisposeInternal(); Initialize(); return FrameCount; } catch (Exception ex) { LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX."); MajorCrashHappened = true; OnError.Invoke(ex); return FrameCount; } finally { try { //Only release the frame if there was a success in capturing it. if (res.Success) DuplicatedOutput.ReleaseFrame(); } catch (Exception e) { LogWriter.Log(e, "It was not possible to release the frame."); } } } public override async Task ManualCaptureAsync(FrameInfo frame, bool showCursor = false) { return await Task.Factory.StartNew(() => ManualCapture(frame, showCursor)); } protected internal bool GetCursor(Texture2D screenTexture, OutputDuplicateFrameInformation info, FrameInfo frame) { //Prepare buffer array to hold the cursor shape. if (CursorShapeBuffer == null || info.PointerShapeBufferSize > CursorShapeBuffer.Length) CursorShapeBuffer = new byte[info.PointerShapeBufferSize]; //If there's a cursor shape available to be captured. if (info.PointerShapeBufferSize > 0) { //Pin the buffer in order to pass the address as parameter later. var pinnedBuffer = GCHandle.Alloc(CursorShapeBuffer, GCHandleType.Pinned); var cursorBufferAddress = pinnedBuffer.AddrOfPinnedObject(); //Load the cursor shape into the buffer. DuplicatedOutput.GetFramePointerShape(info.PointerShapeBufferSize, cursorBufferAddress, out _, out CursorShapeInfo); //If the cursor is monochrome, it will return the cursor shape twice, one is the mask. if (CursorShapeInfo.Type == 1) CursorShapeInfo.Height /= 2; //The buffer must be unpinned, to free resources. pinnedBuffer.Free(); } //Store the current cursor position, if it was moved. if (info.LastMouseUpdateTime != 0) PreviousPosition = info.PointerPosition; //TODO: In a future version, don't merge the cursor image in here, let the editor do that. //Saves the position of the cursor, so the editor can add the mouse events overlay later. frame.CursorX = PreviousPosition.Position.X - (Left - OffsetLeft); frame.CursorY = PreviousPosition.Position.Y - (Top - OffsetTop); //If the method is supposed to simply the get the cursor shape no shape was loaded before, there's nothing else to do. //if (CursorShapeBuffer?.Length == 0 || (info.LastPresentTime == 0 && info.LastMouseUpdateTime == 0) || !info.PointerPosition.Visible) if (screenTexture == null || CursorShapeBuffer?.Length == 0)// || !info.PointerPosition.Visible) { //FallbackCursorCapture(frame); //if (CursorShapeBuffer != null) return false; } //Don't let it bleed beyond the top-left corner, calculate the dimensions of the portion of the cursor that will appear. var leftCut = frame.CursorX; var topCut = frame.CursorY; var rightCut = screenTexture.Description.Width - (frame.CursorX + CursorShapeInfo.Width); var bottomCut = screenTexture.Description.Height - (frame.CursorY + CursorShapeInfo.Height); //Adjust to the hotspot offset, so it's possible to add the highlight correctly later. frame.CursorX += CursorShapeInfo.HotSpot.X; frame.CursorY += CursorShapeInfo.HotSpot.Y; //Don't try merging the textures if the cursor is out of bounds. if (leftCut + CursorShapeInfo.Width < 1 || topCut + CursorShapeInfo.Height < 1 || rightCut + CursorShapeInfo.Width < 1 || bottomCut + CursorShapeInfo.Height < 1) return false; var cursorLeft = Math.Max(leftCut, 0); var cursorTop = Math.Max(topCut, 0); var cursorWidth = leftCut < 0 ? CursorShapeInfo.Width + leftCut : rightCut < 0 ? CursorShapeInfo.Width + rightCut : CursorShapeInfo.Width; var cursorHeight = topCut < 0 ? CursorShapeInfo.Height + topCut : bottomCut < 0 ? CursorShapeInfo.Height + bottomCut : CursorShapeInfo.Height; //The staging texture must be able to hold all pixels. if (CursorStagingTexture == null || CursorStagingTexture.Description.Width != cursorWidth || CursorStagingTexture.Description.Height != cursorHeight) { //In order to change the size of the texture, I need to instantiate it again with the new size. CursorStagingTexture?.Dispose(); CursorStagingTexture = new Texture2D(Device, new Texture2DDescription { ArraySize = 1, BindFlags = BindFlags.None, CpuAccessFlags = CpuAccessFlags.Write, Height = cursorHeight, Format = Format.B8G8R8A8_UNorm, Width = cursorWidth, MipLevels = 1, OptionFlags = ResourceOptionFlags.None, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Staging }); } //The region where the cursor is located is copied to the staging texture to act as the background when dealing with masks and transparency. //The cutout must be the exact region needed and it can't overflow. It's not allowed to try to cut outside of the screenTexture region. var region = new ResourceRegion { Left = cursorLeft, Top = cursorTop, Front = 0, Right = cursorLeft + cursorWidth, Bottom = cursorTop + cursorHeight, Back = 1 }; //Copy from the screen the region in which the cursor is located. Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, region, CursorStagingTexture, 0); //Get cursor details and draw it to the staging texture. DrawCursorShape(CursorStagingTexture, CursorShapeInfo, CursorShapeBuffer, leftCut < 0 ? leftCut * -1 : 0, topCut < 0 ? topCut * -1 : 0, cursorWidth, cursorHeight); //Copy back the cursor texture to the screen texture. Device.ImmediateContext.CopySubresourceRegion(CursorStagingTexture, 0, null, screenTexture, 0, cursorLeft, cursorTop); return true; } private void DrawCursorShape(Texture2D texture, OutputDuplicatePointerShapeInformation info, byte[] buffer, int leftCut, int topCut, int cursorWidth, int cursorHeight) { using (var surface = texture.QueryInterface()) { //Maps the surface, indicating that the CPU needs access to the data. var rect = surface.Map(SharpDX.DXGI.MapFlags.Write); //Cursors can be divided into 3 types: switch (info.Type) { //Masked monochrome, a cursor which reacts with the background. case (int)OutputDuplicatePointerShapeType.Monochrome: DrawMonochromeCursor(leftCut, topCut, cursorWidth, cursorHeight, rect, info.Pitch, buffer, info.Height); break; //Color, a colored cursor which supports transparency. case (int)OutputDuplicatePointerShapeType.Color: DrawColorCursor(leftCut, topCut, cursorWidth, cursorHeight, rect, info.Pitch, buffer); break; //Masked color, a mix of both previous types. case (int)OutputDuplicatePointerShapeType.MaskedColor: DrawMaskedColorCursor(leftCut, topCut, cursorWidth, cursorHeight, rect, info.Pitch, buffer); break; } surface.Unmap(); } } private void DrawMonochromeCursor(int offsetX, int offsetY, int width, int height, DataRectangle rect, int pitch, byte[] buffer, int actualHeight) { for (var row = offsetY; row < height; row++) { //128 in binary. byte mask = 0x80; //Simulate the offset, adjusting the mask. for (var off = 0; off < offsetX; off++) { if (mask == 0x01) mask = 0x80; else mask = (byte)(mask >> 1); } for (var col = offsetX; col < width; col++) { var pos = (row - offsetY) * rect.Pitch + (col - offsetX) * 4; var and = (buffer[row * pitch + col / 8] & mask) == mask; //Mask is take from the first half of the cursor image. var xor = (buffer[row * pitch + col / 8 + actualHeight * pitch] & mask) == mask; //Mask is taken from the second half of the cursor image, hence the "+ height * pitch". //Reads current pixel and applies AND and XOR. (AND/XOR ? White : Black) Marshal.WriteByte(rect.DataPointer, pos, (byte)((Marshal.ReadByte(rect.DataPointer, pos) & (and ? 255 : 0)) ^ (xor ? 255 : 0))); Marshal.WriteByte(rect.DataPointer, pos + 1, (byte)((Marshal.ReadByte(rect.DataPointer, pos + 1) & (and ? 255 : 0)) ^ (xor ? 255 : 0))); Marshal.WriteByte(rect.DataPointer, pos + 2, (byte)((Marshal.ReadByte(rect.DataPointer, pos + 2) & (and ? 255 : 0)) ^ (xor ? 255 : 0))); Marshal.WriteByte(rect.DataPointer, pos + 3, (byte)((Marshal.ReadByte(rect.DataPointer, pos + 3) & 255) ^ 0)); //Shifts the mask around until it reaches 1, then resets it back to 128. if (mask == 0x01) mask = 0x80; else mask = (byte)(mask >> 1); } } } private void DrawColorCursor(int offsetX, int offsetY, int width, int height, DataRectangle rect, int pitch, byte[] buffer) { for (var row = offsetY; row < height; row++) { for (var col = offsetX; col < width; col++) { var surfaceIndex = (row - offsetY) * rect.Pitch + (col - offsetX) * 4; var bufferIndex = row * pitch + col * 4; var alpha = buffer[bufferIndex + 3] + 1; if (alpha == 1) continue; //Premultiplied alpha values. var invAlpha = 256 - alpha; alpha += 1; Marshal.WriteByte(rect.DataPointer, surfaceIndex, (byte)((alpha * buffer[bufferIndex] + invAlpha * Marshal.ReadByte(rect.DataPointer, surfaceIndex)) >> 8)); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 1, (byte)((alpha * buffer[bufferIndex + 1] + invAlpha * Marshal.ReadByte(rect.DataPointer, surfaceIndex + 1)) >> 8)); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 2, (byte)((alpha * buffer[bufferIndex + 2] + invAlpha * Marshal.ReadByte(rect.DataPointer, surfaceIndex + 2)) >> 8)); } } } private void DrawMaskedColorCursor(int offsetX, int offsetY, int width, int height, DataRectangle rect, int pitch, byte[] buffer) { //ImageUtil.ImageMethods.SavePixelArrayToFile(buffer, width, height, 4, System.IO.Path.GetFullPath(".\\MaskedColor.png")); for (var row = offsetY; row < height; row++) { for (var col = offsetX; col < width; col++) { var surfaceIndex = (row - offsetY) * rect.Pitch + (col - offsetX) * 4; var bufferIndex = row * pitch + col * 4; var maskFlag = buffer[bufferIndex + 3]; //Just copies the pixel color. if (maskFlag == 0) { Marshal.WriteByte(rect.DataPointer, surfaceIndex, buffer[bufferIndex]); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 1, buffer[bufferIndex + 1]); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 2, buffer[bufferIndex + 2]); return; } //Applies the XOR operation with the current color. Marshal.WriteByte(rect.DataPointer, surfaceIndex, (byte)(buffer[bufferIndex] ^ Marshal.ReadByte(rect.DataPointer, surfaceIndex))); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 1, (byte)(buffer[bufferIndex + 1] ^ Marshal.ReadByte(rect.DataPointer, surfaceIndex + 1))); Marshal.WriteByte(rect.DataPointer, surfaceIndex + 2, (byte)(buffer[bufferIndex + 2] ^ Marshal.ReadByte(rect.DataPointer, surfaceIndex + 2))); } } } public override void Save(FrameInfo frame) { frame.Image?.Save(frame.Path); frame.Image?.Dispose(); frame.Image = null; Project.Frames.Add(frame); } public override async Task Stop() { if (!WasStarted) return; DisposeInternal(); await base.Stop(); } internal void DisposeInternal() { Device.Dispose(); if (MajorCrashHappened) return; BackingTexture.Dispose(); StagingTexture.Dispose(); DuplicatedOutput.Dispose(); CursorStagingTexture?.Dispose(); } [Obsolete] private void FallbackCursorCapture(FrameInfo frame) { //if (_justStarted && (CursorShapeBuffer?.Length ?? 0) == 0) { //_justStarted = false; //https://stackoverflow.com/a/6374151/1735672 //Bitmap struct, is used to get the cursor shape when SharpDX fails to do so. var infoHeader = new BitmapInfoHeader(); infoHeader.biSize = (uint)Marshal.SizeOf(infoHeader); infoHeader.biBitCount = 32; infoHeader.biClrUsed = 0; infoHeader.biClrImportant = 0; infoHeader.biCompression = 0; infoHeader.biHeight = -Height; //Negative, so the Y-axis will be positioned correctly. infoHeader.biWidth = Width; infoHeader.biPlanes = 1; try { var cursorInfo = new CursorInfo(); cursorInfo.cbSize = Marshal.SizeOf(cursorInfo); if (!User32.GetCursorInfo(out cursorInfo)) return; if (cursorInfo.flags == Native.Constants.CursorShowing) { var hicon = User32.CopyIcon(cursorInfo.hCursor); if (hicon != IntPtr.Zero) { if (User32.GetIconInfo(hicon, out var iconInfo)) { frame.CursorX = cursorInfo.ptScreenPos.X - Left; frame.CursorY = cursorInfo.ptScreenPos.Y - Top; var bitmap = new Bitmap(); var hndl = GCHandle.Alloc(bitmap, GCHandleType.Pinned); var ptrToBitmap = hndl.AddrOfPinnedObject(); Gdi32.GetObject(iconInfo.hbmColor, Marshal.SizeOf(), ptrToBitmap); bitmap = Marshal.PtrToStructure(ptrToBitmap); hndl.Free(); //https://microsoft.public.vc.mfc.narkive.com/H1CZeqUk/how-can-i-get-bitmapinfo-object-from-bitmap-or-hbitmap infoHeader.biHeight = bitmap.bmHeight; infoHeader.biWidth = bitmap.bmWidth; infoHeader.biBitCount = (ushort)bitmap.bmBitsPixel; var w = (bitmap.bmWidth * bitmap.bmBitsPixel + 31) / 8; CursorShapeBuffer = new byte[w * bitmap.bmHeight]; var windowDeviceContext = User32.GetWindowDC(IntPtr.Zero); var compatibleBitmap = Gdi32.CreateCompatibleBitmap(windowDeviceContext, Width, Height); Gdi32.GetDIBits(windowDeviceContext, compatibleBitmap, 0, (uint)infoHeader.biHeight, CursorShapeBuffer, ref infoHeader, DibColorModes.RgbColors); //CursorShapeInfo = new OutputDuplicatePointerShapeInformation(); //CursorShapeInfo.Type = (int)OutputDuplicatePointerShapeType.Color; //CursorShapeInfo.Width = bitmap.bmWidth; //CursorShapeInfo.Height = bitmap.bmHeight; //CursorShapeInfo.Pitch = w; //CursorShapeInfo.HotSpot = new RawPoint(0, 0); //if (frame.CursorX > 0 && frame.CursorY > 0) // Native.DrawIconEx(_compatibleDeviceContext, frame.CursorX - iconInfo.xHotspot, frame.CursorY - iconInfo.yHotspot, cursorInfo.hCursor, 0, 0, 0, IntPtr.Zero, 0x0003); //Native.SelectObject(CompatibleDeviceContext, OldBitmap); //Native.DeleteObject(compatibleBitmap); //Native.DeleteDC(CompatibleDeviceContext); //Native.ReleaseDC(IntPtr.Zero, windowDeviceContext); } Gdi32.DeleteObject(iconInfo.hbmColor); Gdi32.DeleteObject(iconInfo.hbmMask); } User32.DestroyIcon(hicon); } Gdi32.DeleteObject(cursorInfo.hCursor); } catch (Exception e) { LogWriter.Log(e, "Impossible to get the cursor"); } } } } ================================================ FILE: ScreenToGif/Capture/ICapture.cs ================================================ using System; using System.Threading.Tasks; using ScreenToGif.Model; namespace ScreenToGif.Capture; internal interface ICapture : IAsyncDisposable, IDisposable { bool WasStarted { get; set; } int FrameCount { get; set; } int MinimumDelay { get; set; } int Left { get; set; } int Top { get; set; } int Width { get; set; } int Height { get; set; } string DeviceName { get; set; } ProjectInfo Project { get; set; } Action OnError {get;set;} void Start(int delay, int left, int top, int width, int height, double dpi, ProjectInfo project); void ResetConfiguration(); int Capture(FrameInfo frame); Task CaptureAsync(FrameInfo frame); int CaptureWithCursor(FrameInfo frame); Task CaptureWithCursorAsync(FrameInfo frame); int ManualCapture(FrameInfo frame, bool showCursor = false); Task ManualCaptureAsync(FrameInfo frame, bool showCursor = false); void Save(FrameInfo info); Task Stop(); } ================================================ FILE: ScreenToGif/Capture/ImageCapture.cs ================================================ using System; using System.Runtime.InteropServices; using System.Threading.Tasks; using ScreenToGif.Domain.Enums.Native; using ScreenToGif.Model; using ScreenToGif.Native.External; using ScreenToGif.Native.Structs; using ScreenToGif.Util; using ScreenToGif.Util.Settings; using Image = System.Drawing.Image; namespace ScreenToGif.Capture; internal class ImageCapture : BaseCapture { #region Variables private readonly IntPtr _desktopWindow = IntPtr.Zero; protected IntPtr WindowDeviceContext; protected IntPtr CompatibleDeviceContext; protected IntPtr CompatibleBitmap; private IntPtr _oldBitmap; protected int CursorStep { get; set; } private CopyPixelOperations PixelOperations { get; set; } #endregion public override void Start(int delay, int left, int top, int width, int height, double scale, ProjectInfo project) { base.Start(delay, left, top, width, height, scale, project); #region Pointers //http://winprog.org/tutorial/bitmaps.html //_desktopWindow = User32.GetDesktopWindow(); WindowDeviceContext = User32.GetWindowDC(_desktopWindow); CompatibleDeviceContext = Gdi32.CreateCompatibleDC(WindowDeviceContext); CompatibleBitmap = Gdi32.CreateCompatibleBitmap(WindowDeviceContext, Width, Height); _oldBitmap = Gdi32.SelectObject(CompatibleDeviceContext, CompatibleBitmap); #endregion var pixelOp = CopyPixelOperations.SourceCopy; //If not in a remote desktop connection or if the improvement was disabled, capture layered windows too. if (!System.Windows.Forms.SystemInformation.TerminalServerSession || !UserSettings.All.RemoteImprovement) pixelOp |= CopyPixelOperations.CaptureBlt; PixelOperations = pixelOp; } public override int Capture(FrameInfo frame) { try { //var success = Native.BitBlt(CompatibleDeviceContext, 0, 0, Width, Height, WindowDeviceContext, Left, Top, Native.CopyPixelOperation.SourceCopy | Native.CopyPixelOperation.CaptureBlt); var success = Gdi32.StretchBlt(CompatibleDeviceContext, 0, 0, StartWidth, StartHeight, WindowDeviceContext, Left, Top, Width, Height, PixelOperations); if (!success) return FrameCount; //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = Image.FromHbitmap(CompatibleBitmap); if (IsAcceptingFrames) BlockingCollection.Add(frame); } catch (Exception) { //LogWriter.Log(ex, "Impossible to get the screenshot of the screen"); } return FrameCount; } public override async Task CaptureAsync(FrameInfo frame) { return await Task.Factory.StartNew(() => Capture(frame)); } public override int CaptureWithCursor(FrameInfo frame) { try { //var success = Native.BitBlt(CompatibleDeviceContext, 0, 0, Width, Height, WindowDeviceContext, Left, Top, CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt); var success = Gdi32.StretchBlt(CompatibleDeviceContext, 0, 0, StartWidth, StartHeight, WindowDeviceContext, Left, Top, Width, Height, PixelOperations); if (!success) return FrameCount; #region Cursor try { var cursorInfo = new CursorInfo(); cursorInfo.cbSize = Marshal.SizeOf(cursorInfo); if (User32.GetCursorInfo(out cursorInfo)) { if (cursorInfo.flags == Native.Constants.CursorShowing) { var hicon = User32.CopyIcon(cursorInfo.hCursor); if (hicon != IntPtr.Zero) { if (User32.GetIconInfo(hicon, out var iconInfo)) { frame.CursorX = cursorInfo.ptScreenPos.X - Left; frame.CursorY = cursorInfo.ptScreenPos.Y - Top; //(int)(SystemParameters.CursorHeight * Scale) //(int)(SystemParameters.CursorHeight * Scale) var ok = User32.DrawIconEx(CompatibleDeviceContext, frame.CursorX - iconInfo.xHotspot, frame.CursorY - iconInfo.yHotspot, cursorInfo.hCursor, 0, 0, CursorStep, IntPtr.Zero, 0x0003); if (!ok) { CursorStep = 0; User32.DrawIconEx(CompatibleDeviceContext, frame.CursorX - iconInfo.xHotspot, frame.CursorY - iconInfo.yHotspot, cursorInfo.hCursor, 0, 0, CursorStep, IntPtr.Zero, 0x0003); } else CursorStep++; } Gdi32.DeleteObject(iconInfo.hbmColor); Gdi32.DeleteObject(iconInfo.hbmMask); } User32.DestroyIcon(hicon); } Gdi32.DeleteObject(cursorInfo.hCursor); } } catch (Exception) { //LogWriter.Log(e, "Impossible to get the cursor"); } #endregion //Set frame details. FrameCount++; frame.Path = $"{Project.FullPath}{FrameCount}.png"; frame.Delay = FrameRate.GetMilliseconds(); frame.Image = Image.FromHbitmap(CompatibleBitmap); if (IsAcceptingFrames) BlockingCollection.Add(frame); } catch (Exception) { //LogWriter.Log(ex, "Impossible to get the screenshot of the screen"); } return FrameCount; } public override async Task CaptureWithCursorAsync(FrameInfo frame) { return await Task.Factory.StartNew(() => CaptureWithCursor(frame)); } public override void Save(FrameInfo frame) { frame.Image.Save(frame.Path); frame.Image.Dispose(); frame.Image = null; Project.Frames.Add(frame); } public override async Task Stop() { if (!WasStarted) return; await base.Stop(); try { Gdi32.SelectObject(CompatibleDeviceContext, _oldBitmap); Gdi32.DeleteObject(CompatibleBitmap); Gdi32.DeleteDC(CompatibleDeviceContext); User32.ReleaseDC(_desktopWindow, WindowDeviceContext); } catch (Exception e) { LogWriter.Log(e, "Impossible to stop and clean resources used by the recording."); } } } ================================================ FILE: ScreenToGif/Capture/RegionSelectHelper.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Models.Native; using ScreenToGif.Native.Helpers; using ScreenToGif.Windows.Other; namespace ScreenToGif.Capture; internal static class RegionSelectHelper { internal class Selection { public Monitor Monitor { get; set; } public Rect Region { get; set; } public Selection(Monitor monitor, Rect region) { Monitor = monitor; Region = region; } } #region Properties private static TaskCompletionSource _taskCompletionSource; private static readonly List Selectors = new(); internal static bool IsSelecting => Selectors.Any(a => a.IsVisible && a.IsActive); #endregion internal static Task Select(ModeType mode, Rect previousRegion, Monitor currentMonitor, bool quickSelection = false) { _taskCompletionSource = new TaskCompletionSource(); Selectors.Clear(); var monitors = MonitorHelper.AllMonitorsGranular(); //If in quick screen selection mode and there's just one screen, select that one. if (quickSelection && mode == ModeType.Fullscreen && monitors.Count == 1) return Task.FromResult(new Selection(monitors.FirstOrDefault(), monitors[0].Bounds)); foreach (var monitor in monitors) { var selector = new RegionSelector(); selector.Select(monitor, mode, monitor.Handle == currentMonitor?.Handle ? previousRegion : Rect.Empty, RegionSelected, RegionChanged, RegionGotHover, RegionAborted); Selectors.Add(selector); } //Return only when the region gets selected. return _taskCompletionSource.Task; } internal static void Abort() { RegionAborted(); } private static void RegionSelected(Monitor monitor, Rect region) { foreach (var selector in Selectors) selector.CancelSelection(); _taskCompletionSource.SetResult(new Selection(monitor, region)); } private static void RegionChanged(Monitor monitor) { //When one monitor gets the focus, the other ones should be cleaned. foreach (var selector in Selectors.Where(w => w.Monitor.Handle != monitor.Handle)) selector.ClearSelection(); } private static void RegionGotHover(Monitor monitor) { //When one monitor gets the focus, the other ones should be cleaned. foreach (var selector in Selectors.Where(w => w.Monitor.Handle != monitor.Handle)) selector.ClearHoverEffects(); } private static void RegionAborted() { foreach (var selector in Selectors) selector.CancelSelection(); _taskCompletionSource.SetResult(new Selection(null, Rect.Empty)); } } ================================================ FILE: ScreenToGif/Cloud/CloudFactory.cs ================================================ using System; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Interfaces; namespace ScreenToGif.Cloud; public class CloudFactory { public static IUploader CreateCloud(UploadDestinations service) { switch (service) { case UploadDestinations.Imgur: return new Imgur(); case UploadDestinations.Yandex: return new YandexDisk(); } throw new NotImplementedException(); } } ================================================ FILE: ScreenToGif/Cloud/Imgur.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using ScreenToGif.Domain.Interfaces; using ScreenToGif.Domain.Models.Upload.Imgur; using ScreenToGif.Util; using ScreenToGif.ViewModel.UploadPresets.History; using ScreenToGif.ViewModel.UploadPresets.Imgur; using ScreenToGif.Windows.Other; namespace ScreenToGif.Cloud; public class Imgur : IUploader { public async Task UploadFileAsync(IUploadPreset preset, string path, CancellationToken cancellationToken, IProgress progressCallback = null) { if (preset is not ImgurPreset imgurPreset) throw new Exception("Imgur preset is null."); var args = new Dictionary(); var headers = new NameValueCollection(); if (!preset.IsAnonymous) { if (!await IsAuthorized(imgurPreset)) throw new UploadException("It was not possible to get the authorization to upload to Imgur."); headers.Add("Authorization", "Bearer " + imgurPreset.AccessToken); if (imgurPreset.UploadToAlbum) { var album = string.IsNullOrWhiteSpace(imgurPreset.SelectedAlbum) || imgurPreset.SelectedAlbum == "♥♦♣♠" ? await AskForAlbum(imgurPreset) : imgurPreset.SelectedAlbum; if (!string.IsNullOrEmpty(album)) args.Add("album", album); } } else { headers.Add("Authorization", "Client-ID " + Secret.ImgurId); } if (cancellationToken.IsCancellationRequested) return null; return await Upload(imgurPreset, path, args, headers); } public static string GetAuthorizationAdress() { var args = new Dictionary { {"client_id", Secret.ImgurId}, {"response_type", "pin"} }; return WebHelper.AppendQuery("https://api.imgur.com/oauth2/authorize", args); } public static async Task GetTokens(ImgurPreset preset) { var args = new Dictionary { {"client_id", Secret.ImgurId}, {"client_secret", Secret.ImgurSecret}, {"grant_type", "pin"}, {"pin", preset.OAuthToken} }; return await GetTokens(preset, args); } public static async Task RefreshToken(ImgurPreset preset) { var args = new Dictionary { {"refresh_token", preset.RefreshToken}, {"client_id", Secret.ImgurId}, {"client_secret", Secret.ImgurSecret}, {"grant_type", "refresh_token"} }; return await GetTokens(preset, args); } public static bool IsAuthorizationExpired(ImgurPreset preset) { return DateTime.UtcNow > preset.ExpiryDate; } public static async Task IsAuthorized(ImgurPreset preset) { if (string.IsNullOrWhiteSpace(preset.RefreshToken)) return false; if (!IsAuthorizationExpired(preset)) return true; return await RefreshToken(preset); } public static async Task> GetAlbums(ImgurPreset preset) { if (!await IsAuthorized(preset)) return null; var headers = new NameValueCollection { { "Authorization", "Bearer " + preset.AccessToken } }; var response = await WebHelper.Get("https://api.imgur.com/3/account/me/albums", headers); var responseAux = Serializer.Deserialize(response); if (responseAux == null || (!responseAux.Success && responseAux.Status != 200)) return null; var list = responseAux.Data.Select(s => new ImgurAlbum(s)).ToList(); preset.Albums = new ArrayList(list); return list; } public static async Task AskForAlbum(ImgurPreset preset) { var albums = await GetAlbums(preset); return Application.Current.Dispatcher.Invoke(() => PickAlbumDialog.OkCancel(albums)); } private static async Task GetTokens(ImgurPreset preset, Dictionary args) { var response = await WebHelper.PostMultipart("https://api.imgur.com/oauth2/token", args); if (string.IsNullOrEmpty(response)) return false; var token = Serializer.Deserialize(response); if (string.IsNullOrEmpty(token?.AccessToken)) return false; preset.AccessToken = token.AccessToken; preset.RefreshToken = token.RefreshToken; preset.ExpiryDate = DateTime.UtcNow + TimeSpan.FromSeconds(token.ExpiresIn - 10); return true; } private async Task Upload(ImgurPreset preset, string path, Dictionary args, NameValueCollection headers) { await using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { var result = await WebHelper.SendFile("https://api.imgur.com/3/image", stream, path, args, headers, "image"); var response = Serializer.Deserialize(result); //Error when sending video. //{"data":{"errorCode":null,"ticket":"7234557b"},"success":true,"status":200} //{"data":{"error":"No image data was sent to the upload api","request":"\/3\/image","method":"POST"},"success":false,"status":400} if (response == null || (!response.Success && response.Status != 200)) { LogWriter.Log("It was not possible to upload to Imgur", result); return new ImgurHistory { PresetName = preset.Title, DateInUtc = DateTime.UtcNow, Result = 400, Message = response?.Status + " - " + (response?.Data?.Error ?? result) }; } if (string.IsNullOrEmpty(response.Data?.Link)) { LogWriter.Log("It was not possible to upload to Imgur", result); return new ImgurHistory { PresetName = preset.Title, DateInUtc = DateTime.UtcNow, Result = 400, Message = "Upload failed. The link was not provided." }; } var history = new ImgurHistory { PresetName = preset.Title, DateInUtc = DateTime.UtcNow, Result = 200, Id = response.Data.Id, Link = $"https://imgur.com/{response.Data.Id}", DeletionLink = $"https://imgur.com/delete/{response.Data.DeleteHash}", Mp4 = response.Data.Mp4, Webm = response.Data.Webm, Gifv = response.Data.Gifv, Gif = response.Data.Link }; return history; } } } ================================================ FILE: ScreenToGif/Cloud/YandexDisk.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using ScreenToGif.Domain.Interfaces; using ScreenToGif.Domain.Models.Upload.YandexDisk; using ScreenToGif.Util; using ScreenToGif.Util.Settings; using ScreenToGif.ViewModel.UploadPresets.History; using ScreenToGif.ViewModel.UploadPresets.Yandex; namespace ScreenToGif.Cloud; public class YandexDisk : IUploader { public async Task UploadFileAsync(IUploadPreset preset, string path, CancellationToken cancellationToken, IProgress progressCallback = null) { if (string.IsNullOrEmpty(path)) throw new ArgumentException(nameof(path)); var fileName = Path.GetFileName(path); var link = await GetAsync(preset as YandexPreset, "https://cloud-api.yandex.net/v1/disk/resources/upload?path=app:/" + fileName + "&overwrite=true", cancellationToken); if (string.IsNullOrEmpty(link?.Href)) throw new UploadException("Unknown error"); await using (var fileSteram = new FileStream(path, FileMode.Open, FileAccess.Read)) { await PutAsync(preset as YandexPreset, link.Href, new StreamContent(fileSteram), cancellationToken); } var downloadLink = await GetAsync(preset as YandexPreset, "https://cloud-api.yandex.net/v1/disk/resources/download?path=app:/" + fileName, cancellationToken); var history = new History { Type = preset.Type, PresetName = preset.Title, DateInUtc = DateTime.UtcNow, Result = 200, Link = downloadLink.Href }; return history; } private async Task GetAsync(YandexPreset preset, string url, CancellationToken cancellationToken) { var handler = new HttpClientHandler { Proxy = WebHelper.GetProxy(), PreAuthenticate = true, UseDefaultCredentials = false, }; using (var client = new HttpClient(handler)) { var request = new HttpRequestMessage(HttpMethod.Get, url) { Headers = { {HttpRequestHeader.Authorization.ToString(), "OAuth " + preset.OAuthToken} } }; string responseBody; using (var response = await client.SendAsync(request, cancellationToken)) { responseBody = await response.Content.ReadAsStringAsync(cancellationToken); } var errorDescriptor = Serializer.Deserialize(responseBody); if (errorDescriptor.Error != null) throw new UploadException($"{errorDescriptor.Error}, {errorDescriptor.Message}, {errorDescriptor.Description}"); return Serializer.Deserialize(responseBody); } } private async Task PutAsync(YandexPreset preset, string url, HttpContent content, CancellationToken cancellationToken) { var handler = new HttpClientHandler { Proxy = WebHelper.GetProxy(), PreAuthenticate = true, UseDefaultCredentials = false, }; using (var client = new HttpClient(handler)) { var request = new HttpRequestMessage(HttpMethod.Put, url) { Headers = { {HttpRequestHeader.Authorization.ToString(), "OAuth " + preset.OAuthToken} }, Content = content }; using (await client.SendAsync(request, cancellationToken)) { } } } public static string GetAuthorizationAdress() { var args = new Dictionary { {"client_id", Secret.YandexId}, {"response_type", "token"} }; return WebHelper.AppendQuery($"https://oauth.yandex.{(UserSettings.All.LanguageCode.StartsWith("ru") ? "ru" : "com")}/authorize", args); } public static bool IsAuthorized(YandexPreset preset) { return !string.IsNullOrWhiteSpace(preset.OAuthToken); } } ================================================ FILE: ScreenToGif/Controls/AdornerControl.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls; /// /// A content control that allows an adorner for the content to /// be defined in XAML. /// public class AdornedControl : ContentControl { #region Dependency Properties public static readonly DependencyProperty IsAdornerVisibleProperty = DependencyProperty.Register("IsAdornerVisible", typeof(bool), typeof(AdornedControl), new FrameworkPropertyMetadata(IsAdornerVisible_PropertyChanged)); public static readonly DependencyProperty AdornerContentProperty = DependencyProperty.Register("AdornerContent", typeof(FrameworkElement), typeof(AdornedControl), new FrameworkPropertyMetadata(AdornerContent_PropertyChanged)); public static readonly DependencyProperty HorizontalAdornerPlacementProperty = DependencyProperty.Register("HorizontalAdornerPlacement", typeof(AdornerPlacement), typeof(AdornedControl), new FrameworkPropertyMetadata(AdornerPlacement.Outside)); public static readonly DependencyProperty VerticalAdornerPlacementProperty = DependencyProperty.Register("VerticalAdornerPlacement", typeof(AdornerPlacement), typeof(AdornedControl), new FrameworkPropertyMetadata(AdornerPlacement.Outside)); public static readonly DependencyProperty AdornerOffsetXProperty = DependencyProperty.Register("AdornerOffsetX", typeof(double), typeof(AdornedControl)); public static readonly DependencyProperty AdornerOffsetYProperty = DependencyProperty.Register("AdornerOffsetY", typeof(double), typeof(AdornedControl)); #endregion Dependency Properties #region Properties /// /// Used in XAML to define the UI content of the adorner. /// public FrameworkElement AdornerContent { get => (FrameworkElement)GetValue(AdornerContentProperty); set => SetValue(AdornerContentProperty, value); } /// /// Specifies the horizontal placement of the adorner relative to the adorned control. /// public AdornerPlacement HorizontalAdornerPlacement { get => (AdornerPlacement)GetValue(HorizontalAdornerPlacementProperty); set => SetValue(HorizontalAdornerPlacementProperty, value); } /// /// Specifies the vertical placement of the adorner relative to the adorned control. /// public AdornerPlacement VerticalAdornerPlacement { get => (AdornerPlacement)GetValue(VerticalAdornerPlacementProperty); set => SetValue(VerticalAdornerPlacementProperty, value); } /// /// X offset of the adorner. /// public double AdornerOffsetX { get => (double)GetValue(AdornerOffsetXProperty); set => SetValue(AdornerOffsetXProperty, value); } /// /// Y offset of the adorner. /// public double AdornerOffsetY { get => (double)GetValue(AdornerOffsetYProperty); set => SetValue(AdornerOffsetYProperty, value); } #endregion #region Commands public static readonly RoutedCommand ShowAdornerCommand = new RoutedCommand("ShowAdorner", typeof(AdornedControl)); public static readonly RoutedCommand HideAdornerCommand = new RoutedCommand("HideAdorner", typeof(AdornedControl)); #endregion Commands static AdornedControl() { CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), ShowAdornerCommandBinding); CommandManager.RegisterClassCommandBinding(typeof(AdornedControl), HideAdornerCommandBinding); } public AdornedControl() { Focusable = false; // By default don't want 'AdornedControl' to be focusable. DataContextChanged += AdornedControl_DataContextChanged; } /// /// Event raised when the DataContext of the adorned control changes. /// private void AdornedControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { UpdateAdornerDataContext(); } /// /// Update the DataContext of the adorner from the adorned control. /// private void UpdateAdornerDataContext() { if (AdornerContent != null) AdornerContent.DataContext = DataContext; } /// /// Show the adorner. /// public void ShowAdorner() { IsAdornerVisible = true; } /// /// Hide the adorner. /// public void HideAdorner() { IsAdornerVisible = false; } /// /// Shows or hides the adorner. /// Set to 'true' to show the adorner or 'false' to hide the adorner. /// public bool IsAdornerVisible { get => (bool)GetValue(IsAdornerVisibleProperty); set => SetValue(IsAdornerVisibleProperty, value); } #region Private Data Members /// /// Command bindings. /// private static readonly CommandBinding ShowAdornerCommandBinding = new CommandBinding(ShowAdornerCommand, ShowAdornerCommand_Executed); private static readonly CommandBinding HideAdornerCommandBinding = new CommandBinding(HideAdornerCommand, HideAdornerCommand_Executed); /// /// Caches the adorner layer. /// private AdornerLayer _adornerLayer = null; /// /// The actual adorner create to contain our 'adorner UI content'. /// private FrameworkElementAdorner _adorner = null; #endregion #region Private/Internal Functions /// /// Event raised when the Show command is executed. /// private static void ShowAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) { var c = (AdornedControl)target; c.ShowAdorner(); } /// /// Event raised when the Hide command is executed. /// private static void HideAdornerCommand_Executed(object target, ExecutedRoutedEventArgs e) { var c = (AdornedControl)target; c.HideAdorner(); } /// /// Event raised when the value of IsAdornerVisible has changed. /// private static void IsAdornerVisible_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var c = (AdornedControl)o; c.ShowOrHideAdornerInternal(); } /// /// Event raised when the value of AdornerContent has changed. /// private static void AdornerContent_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var c = (AdornedControl)o; c.ShowOrHideAdornerInternal(); } /// /// Internal method to show or hide the adorner based on the value of IsAdornerVisible. /// private void ShowOrHideAdornerInternal() { if (IsAdornerVisible) ShowAdornerInternal(); else HideAdornerInternal(); } /// /// Internal method to show the adorner. /// private void ShowAdornerInternal() { if (_adorner != null) { // Already adorned. return; } if (AdornerContent == null) return; if (_adornerLayer == null) _adornerLayer = AdornerLayer.GetAdornerLayer(this); if (_adornerLayer != null) { _adorner = new FrameworkElementAdorner(AdornerContent, this, HorizontalAdornerPlacement, VerticalAdornerPlacement, AdornerOffsetX, AdornerOffsetY); _adornerLayer.Add(_adorner); UpdateAdornerDataContext(); } } /// /// Internal method to hide the adorner. /// private void HideAdornerInternal() { if (_adornerLayer == null || _adorner == null) { // Not already adorned. return; } _adornerLayer.Remove(_adorner); _adorner.DisconnectChild(); _adorner = null; _adornerLayer = null; } #endregion } ================================================ FILE: ScreenToGif/Controls/AttachmentListBoxItem.cs ================================================ using System; using System.ComponentModel; using System.Drawing; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media.Imaging; namespace ScreenToGif.Controls; public class AttachmentListBoxItem : ListBoxItem { #region Dependency Property public static readonly DependencyProperty ShortNameProperty = DependencyProperty.Register("ShortName", typeof(string), typeof(AttachmentListBoxItem), new FrameworkPropertyMetadata("Something", FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty AttachmentProperty = DependencyProperty.Register("Attachment", typeof(string), typeof(AttachmentListBoxItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure, AttachmentChangedCallback)); public static readonly DependencyProperty FileIconProperty = DependencyProperty.Register("FileIcon", typeof(BitmapSource), typeof(AttachmentListBoxItem)); #endregion #region Property Accessor [Bindable(true), Category("Common")] public string ShortName { get => (string)GetValue(ShortNameProperty); private set => SetValue(ShortNameProperty, value); } [Bindable(true), Category("Common")] public string Attachment { get => (string)GetValue(AttachmentProperty); private set => SetValue(AttachmentProperty, value); } [Bindable(true), Category("Common")] public BitmapSource FileIcon { get => (BitmapSource)GetValue(FileIconProperty); private set => SetValue(FileIconProperty, value); } #endregion static AttachmentListBoxItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(AttachmentListBoxItem), new FrameworkPropertyMetadata(typeof(AttachmentListBoxItem))); } public AttachmentListBoxItem(string attachment) { Attachment = attachment; } private static void AttachmentChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { var item = d as AttachmentListBoxItem; if (item == null) return; if (!File.Exists(item.Attachment)) return; item.ShortName = Path.GetFileName(item.Attachment); using (var icon = Icon.ExtractAssociatedIcon(item.Attachment)) { if (icon == null) return; item.FileIcon = Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } GC.Collect(1); item.UpdateLayout(); } } ================================================ FILE: ScreenToGif/Controls/AwareTabItem.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; public class AwareTabItem : TabItem { #region Dependency Property public static readonly DependencyProperty IsDarkProperty = DependencyProperty.Register(nameof(IsDark), typeof(bool), typeof(AwareTabItem), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, OnPropertyChanged)); public static readonly DependencyProperty ShowBackgroundProperty = DependencyProperty.Register(nameof(ShowBackground), typeof(bool), typeof(AwareTabItem), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, ShowBackground_OnPropertyChanged)); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(AwareTabItem)); #endregion #region Property accessors /// /// True if the titlebar color is dark. /// [Bindable(true), Category("Appearance")] public bool IsDark { get => (bool)GetValue(IsDarkProperty); set => SetValue(IsDarkProperty, value); } /// /// True if should display the background of the tab while not selected. /// [Bindable(true), Category("Appearance")] public bool ShowBackground { get => (bool)GetValue(ShowBackgroundProperty); set => SetValue(ShowBackgroundProperty, value); } /// /// The icon of the tab. /// [Description("The icon of the tab.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } #endregion static AwareTabItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(AwareTabItem), new FrameworkPropertyMetadata(typeof(AwareTabItem))); } /// /// This method is called when any of our dependency properties change. /// /// Dependency Object /// EventArgs private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((AwareTabItem)d).IsDark = (bool)e.NewValue; } /// /// This method is called when any of our dependency properties change. /// /// Dependency Object /// EventArgs private static void ShowBackground_OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((AwareTabItem)d).ShowBackground = (bool)e.NewValue; } } ================================================ FILE: ScreenToGif/Controls/BaseRecorder.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Input; using ScreenToGif.Domain.Enums; using ScreenToGif.Model; namespace ScreenToGif.Controls; /// /// All recorders are derived from this class. /// public class BaseRecorder : Window { public static readonly DependencyProperty StageProperty = DependencyProperty.Register(nameof(Stage), typeof(RecorderStages), typeof(BaseRecorder), new FrameworkPropertyMetadata(RecorderStages.Stopped)); public static readonly DependencyProperty FrameCountProperty = DependencyProperty.Register(nameof(FrameCount), typeof(int), typeof(BaseRecorder), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty HasImpreciseCaptureProperty = DependencyProperty.Register(nameof(HasImpreciseCapture), typeof(bool), typeof(BaseRecorder), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); /// /// The actual stage of the recorder. /// public RecorderStages Stage { get => (RecorderStages)GetValue(StageProperty); set { SetValue(StageProperty, value); CommandManager.InvalidateRequerySuggested(); } } /// /// The frame count of the current recording. /// [Bindable(true), Category("Common"), Description("The frame count of the current recording.")] public int FrameCount { get => (int)GetValue(FrameCountProperty); set => SetValue(FrameCountProperty, value); } /// /// The frame count of the current recording. /// [Bindable(true), Category("Common"), Description("True if the recorder is unable to capture with precision.")] public bool HasImpreciseCapture { get => (bool)GetValue(HasImpreciseCaptureProperty); set => SetValue(HasImpreciseCaptureProperty, value); } /// /// The project information about the current recording. /// internal ProjectInfo Project { get; set; } } ================================================ FILE: ScreenToGif/Controls/BaseScreenRecorder.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using ScreenToGif.Capture; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Interfaces; using ScreenToGif.Model; using ScreenToGif.Native.Helpers; using ScreenToGif.Util; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; public class BaseScreenRecorder : BaseRecorder { #region Variables /// /// The token in use to control the execution of the capture. /// private CancellationTokenSource _captureToken; /// /// Indicates when the user is mouse-clicking. /// internal MouseButtons RecordClicked = MouseButtons.None; /// /// Deals with all screen capture methods. /// internal ICapture Capture; /// /// Lists of pressed keys. /// internal readonly List KeyList = new(); /// /// Timer responsible for the forced clean up of the objects in memory. /// internal readonly System.Timers.Timer GarbageTimer = new System.Timers.Timer(); #endregion public BaseScreenRecorder() { GarbageTimer.Interval = 3000; GarbageTimer.Elapsed += GarbageTimer_Tick; } private void GarbageTimer_Tick(object sender, EventArgs e) { GC.Collect(2); } internal bool HasFixedDelay() { return UserSettings.All.CaptureFrequency != CaptureFrequencies.PerSecond || UserSettings.All.FixedFrameRate; } internal int GetFixedDelay() { switch (UserSettings.All.CaptureFrequency) { case CaptureFrequencies.Manual: return UserSettings.All.PlaybackDelayManual; case CaptureFrequencies.Interaction: return UserSettings.All.PlaybackDelayInteraction; case CaptureFrequencies.PerMinute: return UserSettings.All.PlaybackDelayMinute; case CaptureFrequencies.PerHour: return UserSettings.All.PlaybackDelayHour; default: //When the capture is 'PerSecond', the fixed delay is set to use the current framerate. return 1000 / UserSettings.All.LatestFps; } } internal int GetTriggerDelay() { switch (UserSettings.All.CaptureFrequency) { case CaptureFrequencies.Interaction: return UserSettings.All.TriggerDelayInteraction; case CaptureFrequencies.Manual: return UserSettings.All.TriggerDelayManual; default: return 0; } } internal int GetCaptureInterval() { switch (UserSettings.All.CaptureFrequency) { case CaptureFrequencies.PerHour: //15 frames per hour = 240,000 ms (240 sec, 4 min). return (1000 * 60 * 60) / UserSettings.All.LatestFps; case CaptureFrequencies.PerMinute: //15 frames per minute = 4,000 ms (4 sec). return (1000 * 60) / UserSettings.All.LatestFps; default: //PerSecond. 15 frames per second = 66 ms. return 1000 / UserSettings.All.LatestFps; } } internal ICapture GetDirectCapture() { if (UserSettings.All.OnlyCaptureChanges) return UserSettings.All.UseMemoryCache ? (ICapture)new DirectChangedCachedCapture() : new DirectChangedImageCapture(); return UserSettings.All.UseMemoryCache ? new DirectCachedCapture() : new DirectImageCapture(); } internal virtual void StartCapture() { FrameRate.Start(HasFixedDelay(), GetFixedDelay()); HasImpreciseCapture = false; if (UserSettings.All.ForceGarbageCollection) GarbageTimer.Start(); lock (UserSettings.Lock) { //Starts the capture. _captureToken = new CancellationTokenSource(); Task.Run(() => PrepareCaptureLoop(GetCaptureInterval()), _captureToken.Token); } } internal virtual void PauseCapture() { FrameRate.Stop(); StopInternalCapture(); } internal virtual async Task StopCapture() { FrameRate.Stop(); StopInternalCapture(); if (Capture != null) await Capture.Stop(); GarbageTimer.Stop(); } private void StopInternalCapture() { if (_captureToken == null) return; _captureToken.Cancel(); _captureToken.Dispose(); _captureToken = null; } private void PrepareCaptureLoop(int interval) { using (var resolution = new TimerResolution(1)) { if (!resolution.SuccessfullySetTargetResolution) { LogWriter.Log($"Imprecise timer resolution... Target: {resolution.TargetResolution}, Current: {resolution.CurrentResolution}"); Dispatcher.Invoke(() => HasImpreciseCapture = true); } if (UserSettings.All.ShowCursor) CaptureWithCursor(interval); else CaptureWithoutCursor(interval); Dispatcher.Invoke(() => HasImpreciseCapture = false); } } private void CaptureWithCursor(int interval) { var sw = new Stopwatch(); while (_captureToken != null && !_captureToken.IsCancellationRequested) { sw.Restart(); //Capture frame. var frame = new FrameInfo(RecordClicked, KeyList); KeyList.Clear(); var frameCount = Capture.CaptureWithCursor(frame); Dispatcher.Invoke(() => FrameCount = frameCount); //If behind wait time, wait before capturing new frame. if (sw.ElapsedMilliseconds >= interval) continue; while (sw.Elapsed.TotalMilliseconds < interval) Thread.Sleep(1); //SpinWait.SpinUntil(() => sw.ElapsedMilliseconds >= interval); } sw.Stop(); } private void CaptureWithoutCursor(int interval) { var sw = new Stopwatch(); while (_captureToken != null && !_captureToken.IsCancellationRequested) { sw.Restart(); //Capture frame. var frame = new FrameInfo(RecordClicked, KeyList); KeyList.Clear(); var frameCount = Capture.Capture(frame); Dispatcher.Invoke(() => FrameCount = frameCount); //If behind wait time, wait before capturing new frame. if (sw.ElapsedMilliseconds >= interval) continue; while (sw.Elapsed.TotalMilliseconds < interval) Thread.Sleep(1); //SpinWait.SpinUntil(() => sw.ElapsedMilliseconds >= interval); } sw.Stop(); } } ================================================ FILE: ScreenToGif/Controls/BaseWindow.cs ================================================ using System; using System.Windows; namespace ScreenToGif.Controls; public class BaseWindow : Window { public DateTime CreationIn { get; set; } public DateTime NonMinimizedIn { get; set; } public DateTime MinimizedIn { get; set; } public BaseWindow() { NonMinimizedIn = CreationIn = DateTime.Now; } protected override void OnStateChanged(EventArgs e) { if (WindowState != WindowState.Minimized) NonMinimizedIn = DateTime.Now; else MinimizedIn = DateTime.Now; base.OnStateChanged(e); } } ================================================ FILE: ScreenToGif/Controls/Card.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Media; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls; public class Card : Button { #region Dependency Properties public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(Card)); public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(nameof(Header), typeof(string), typeof(Card)); public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(Card)); public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(nameof(Status), typeof(ExtrasStatus), typeof(Card), new PropertyMetadata(ExtrasStatus.Available)); #endregion #region Property Accessors public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetValue(IconProperty, value); } public string Header { get => (string)GetValue(HeaderProperty); set => SetValue(HeaderProperty, value); } public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } public ExtrasStatus Status { get => (ExtrasStatus)GetValue(StatusProperty); set => SetValue(StatusProperty, value); } #endregion static Card() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Card), new FrameworkPropertyMetadata(typeof(Card))); } } ================================================ FILE: ScreenToGif/Controls/CircularProgressBar.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace ScreenToGif.Controls; internal class CircularProgressBar : ProgressBar { #region Variables and properties private Path _pathRoot; private PathFigure _pathFigure; private ArcSegment _arcSegment; public static readonly DependencyProperty PercentageProperty = DependencyProperty.Register(nameof(Percentage), typeof(double), typeof(CircularProgressBar), new PropertyMetadata(OnPercentageChanged)); public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(nameof(StrokeThickness), typeof(double), typeof(CircularProgressBar), new PropertyMetadata(5D, OnPropertyChanged)); public static readonly DependencyProperty SegmentColorProperty = DependencyProperty.Register(nameof(SegmentColor), typeof(Brush), typeof(CircularProgressBar), new PropertyMetadata(Brushes.Red)); public static readonly DependencyProperty RadiusProperty = DependencyProperty.Register(nameof(Radius), typeof(double), typeof(CircularProgressBar), new PropertyMetadata(25D, OnPropertyChanged)); public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(nameof(Angle), typeof(double), typeof(CircularProgressBar), new PropertyMetadata(120D, OnPropertyChanged)); public static readonly DependencyProperty IsInvertedProperty = DependencyProperty.Register(nameof(IsInverted), typeof(bool), typeof(CircularProgressBar), new PropertyMetadata(false, OnPropertyChanged)); public double Radius { get => (double)GetValue(RadiusProperty); set => SetValue(RadiusProperty, value); } public Brush SegmentColor { get => (Brush)GetValue(SegmentColorProperty); set => SetValue(SegmentColorProperty, value); } public double StrokeThickness { get => (double)GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public double Percentage { get => (double)GetValue(PercentageProperty); set => SetValue(PercentageProperty, value); } public double Angle { get => (double)GetValue(AngleProperty); set => SetValue(AngleProperty, value); } public bool IsInverted { get => (bool)GetValue(IsInvertedProperty); set => SetValue(IsInvertedProperty, value); } #endregion static CircularProgressBar() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CircularProgressBar), new FrameworkPropertyMetadata(typeof(CircularProgressBar))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); ValueChanged += CircularProgressBar_ValueChanged; _pathRoot = Template.FindName("PathRoot", this) as Path; _pathFigure = Template.FindName("PathFigure", this) as PathFigure; _arcSegment = Template.FindName("ArcSegment", this) as ArcSegment; if (Math.Abs(Percentage) < 0.001) { if (IsInverted) Percentage = Math.Abs(100F * (Value - 1) / (Maximum - Minimum) - 100F); else Percentage = (100F * Value) / (Maximum - Minimum); } Angle = (Percentage * 360) / 100; RenderArc(); } #region Events private void CircularProgressBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { if (IsInverted) { Percentage = Math.Abs((100F * (Value - 1)) / (Maximum - Minimum) - 100F); return; } Percentage = (100F * Value) / (Maximum - Minimum); } private static void OnPercentageChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { if (sender is CircularProgressBar circle) circle.Angle = (circle.Percentage * 360) / 100; } private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { var circle = sender as CircularProgressBar; circle?.RenderArc(); } #endregion #region Methods public void RenderArc() { var startPoint = new Point(Radius, 0); var endPoint = ComputeCartesianCoordinate(Angle, Radius); endPoint.X += Radius; endPoint.Y += Radius; if (_pathRoot != null) { _pathRoot.Width = Radius * 2 + StrokeThickness; _pathRoot.Height = Radius * 2 + StrokeThickness; _pathRoot.Margin = new Thickness(StrokeThickness, StrokeThickness, 0, 0); } var largeArc = Angle > 180.0; var outerArcSize = new Size(Radius, Radius); if (_pathFigure != null) _pathFigure.StartPoint = startPoint; if (Math.Abs(startPoint.X - Math.Round(endPoint.X)) < 0.001 && Math.Abs(startPoint.Y - Math.Round(endPoint.Y)) < 0.001) endPoint.X -= 0.01; if (_arcSegment != null) { _arcSegment.Point = endPoint; _arcSegment.Size = outerArcSize; _arcSegment.IsLargeArc = largeArc; } } private Point ComputeCartesianCoordinate(double angle, double radius) { //Convert to radians. var angleRad = (Math.PI / 180.0) * (angle - 90); var x = radius * Math.Cos(angleRad); var y = radius * Math.Sin(angleRad); return new Point(x, y); } #endregion } ================================================ FILE: ScreenToGif/Controls/ColorBox.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Media; using ScreenToGif.Windows.Other; namespace ScreenToGif.Controls; public class ColorBox : ButtonBase { #region Properties public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorBox), new PropertyMetadata(default(Color), SelectedColor_Changed)); public static readonly DependencyProperty SelectedBrushProperty = DependencyProperty.Register(nameof(SelectedBrush), typeof(SolidColorBrush), typeof(ColorBox), new PropertyMetadata(default(SolidColorBrush))); public static readonly DependencyProperty AllowTransparencyProperty = DependencyProperty.Register(nameof(AllowTransparency), typeof(bool), typeof(ColorBox), new PropertyMetadata(true)); public static readonly DependencyProperty IgnoreEventProperty = DependencyProperty.Register(nameof(IgnoreEvent), typeof(bool), typeof(ColorBox), new PropertyMetadata(false)); public Color SelectedColor { get => (Color) GetValue(SelectedColorProperty); set => SetValue(SelectedColorProperty, value); } public SolidColorBrush SelectedBrush { get => (SolidColorBrush)GetValue(SelectedBrushProperty); set => SetValue(SelectedBrushProperty, value); } public bool AllowTransparency { get => (bool)GetValue(AllowTransparencyProperty); set => SetValue(AllowTransparencyProperty, value); } public bool IgnoreEvent { get => (bool)GetValue(IgnoreEventProperty); set => SetValue(IgnoreEventProperty, value); } #endregion static ColorBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorBox), new FrameworkPropertyMetadata(typeof(ColorBox))); } protected override void OnClick() { SelectColor(); base.OnClick(); } private void SelectColor() { var colorPicker = new ColorSelector(SelectedColor, AllowTransparency); var result = colorPicker.ShowDialog(); if (result.HasValue && result.Value) SelectedColor = colorPicker.SelectedColor; } private static void SelectedColor_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is ColorBox box)) return; box.SelectedBrush = new SolidColorBrush(box.SelectedColor); box.RaiseColorChangedEvent(); } #region Custom Events public static readonly RoutedEvent ColorChangedEvent = EventManager.RegisterRoutedEvent(nameof(ColorChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColorBox)); public event RoutedEventHandler ColorChanged { add => AddHandler(ColorChangedEvent, value); remove => RemoveHandler(ColorChangedEvent, value); } public void RaiseColorChangedEvent() { if (ColorChangedEvent == null || IgnoreEvent) return; RaiseEvent(new RoutedEventArgs(ColorChangedEvent)); } #endregion } ================================================ FILE: ScreenToGif/Controls/CroppingAdorner.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using Point = System.Drawing.Point; namespace ScreenToGif.Controls; public class CroppingAdorner : Adorner { #region Private variables /// /// The size of the Thumb in pixels. /// private const double ThumbWidth = 10; /// /// Rectangle Shape, visual aid for the cropping selection. /// private readonly PuncturedRect _cropMask; /// /// Canvas that holds the Thumb collection. /// private readonly Canvas _thumbCanvas; /// /// Corner Thumbs used to change the crop selection. /// private readonly Thumb _thumbTopLeft, _thumbTopRight, _thumbBottomLeft, _thumbBottomRight, _thumbTop, _thumbLeft, _thumbBottom, _thumbRight, _thumbCenter; /// /// Stores and manages the adorner's visual children. /// private readonly VisualCollection _visualCollection; /// /// Screen DPI. /// private static readonly double DpiX, DpiY; #endregion #region Routed Events public static readonly RoutedEvent CropChangedEvent = EventManager.RegisterRoutedEvent( "CropChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CroppingAdorner)); public event RoutedEventHandler CropChanged { add => AddHandler(CropChangedEvent, value); remove => RemoveHandler(CropChangedEvent, value); } #endregion #region Dependency Properties public static DependencyProperty FillProperty = Shape.FillProperty.AddOwner(typeof(CroppingAdorner), new FrameworkPropertyMetadata(new SolidColorBrush(Color.FromArgb(110, 0, 0, 0)), FillPropChanged)); public static readonly DependencyProperty ClipRectangleProperty = DependencyProperty.Register("ClipRectangle", typeof(Rect), typeof(CroppingAdorner), new FrameworkPropertyMetadata(new Rect(0, 0, 0, 0), ClipRectanglePropertyChanged)); public Brush Fill { get => (Brush)GetValue(FillProperty); set => SetValue(FillProperty, value); } public Rect ClipRectangle { get => _cropMask.Interior; set => SetValue(ClipRectangleProperty, value); } private static void FillPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is CroppingAdorner crp) crp._cropMask.Fill = (Brush) e.NewValue; } private static void ClipRectanglePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is CroppingAdorner crp)) return; crp._cropMask.Interior = (Rect)e.NewValue; crp.SetThumbs(crp._cropMask.Interior); crp.RaiseEvent(new RoutedEventArgs(CropChangedEvent, crp)); } #endregion #region Constructor static CroppingAdorner() { using (var g = System.Drawing.Graphics.FromHwnd((IntPtr)0)) { DpiX = g.DpiX; DpiY = g.DpiY; } } public CroppingAdorner(UIElement adornedElement, Rect rcInit) : base(adornedElement) { _cropMask = new PuncturedRect { IsHitTestVisible = false, Interior = rcInit, Fill = Fill, Focusable = true }; _thumbCanvas = new Canvas { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch }; _visualCollection = new VisualCollection(this) { _cropMask, _thumbCanvas }; BuildCorner(ref _thumbTop, Cursors.SizeNS); BuildCorner(ref _thumbBottom, Cursors.SizeNS); BuildCorner(ref _thumbLeft, Cursors.SizeWE); BuildCorner(ref _thumbRight, Cursors.SizeWE); BuildCorner(ref _thumbTopLeft, Cursors.SizeNWSE); BuildCorner(ref _thumbTopRight, Cursors.SizeNESW); BuildCorner(ref _thumbBottomLeft, Cursors.SizeNESW); BuildCorner(ref _thumbBottomRight, Cursors.SizeNWSE); BuildCenter(ref _thumbCenter); _cropMask.PreviewKeyDown += CropMask_PreviewKeyDown; //Cropping handlers. _thumbBottomLeft.DragDelta += HandleBottomLeft; _thumbBottomRight.DragDelta += HandleBottomRight; _thumbTopLeft.DragDelta += HandleTopLeft; _thumbTopRight.DragDelta += HandleTopRight; _thumbTop.DragDelta += HandleTop; _thumbBottom.DragDelta += HandleBottom; _thumbRight.DragDelta += HandleRight; _thumbLeft.DragDelta += HandleLeft; _thumbCenter.DragDelta += HandleCenter; //Clipping interior should be within the bounds of the adorned element. if (adornedElement is FrameworkElement element) element.SizeChanged += AdornedElement_SizeChanged; } private void CropMask_PreviewKeyDown(object sender, KeyEventArgs e) { //Control + Shift: Expand both ways. if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && (Keyboard.Modifiers & ModifierKeys.Shift) != 0) { switch (e.Key) { case Key.Up: HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, 1)); HandleTop(_thumbCenter, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, -1)); HandleTop(_thumbCenter, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleRight(_thumbCenter, new DragDeltaEventArgs(-1, 0)); HandleLeft(_thumbCenter, new DragDeltaEventArgs(1, 0)); break; case Key.Right: HandleRight(_thumbCenter, new DragDeltaEventArgs(1, 0)); HandleLeft(_thumbCenter, new DragDeltaEventArgs(-1, 0)); break; } return; } //If the Shift key is pressed, the sizing mode is enabled (bottom right). if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) { switch (e.Key) { case Key.Up: HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleRight(_thumbCenter, new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleRight(_thumbCenter, new DragDeltaEventArgs(1, 0)); break; } return; } //If the Control key is pressed, the sizing mode is enabled (top left). if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) { switch (e.Key) { case Key.Up: HandleTop(_thumbCenter, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleTop(_thumbCenter, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleLeft(_thumbCenter, new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleLeft(_thumbCenter, new DragDeltaEventArgs(1, 0)); break; } return; } //If no other key is pressed, the movement mode is enabled. switch (e.Key) { case Key.Up: HandleCenter(_thumbCenter, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleCenter(_thumbCenter, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleCenter(_thumbCenter, new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleCenter(_thumbCenter, new DragDeltaEventArgs(1, 0)); break; } } #endregion #region Thumb handlers private void HandleThumb(double drcL, double drcT, double drcW, double drcH, double dx, double dy) { var interior = _cropMask.Interior; if (interior.Width + drcW * dx < 0) dx = -interior.Width / drcW; if (interior.Height + drcH * dy < 0) dy = -interior.Height / drcH; interior = new Rect(interior.Left + drcL * dx, interior.Top + drcT * dy, interior.Width + drcW * dx, interior.Height + drcH * dy); //Minimum of 10x10. if (interior.Width < 10) { if (interior.X + interior.Width > _cropMask.Exterior.Width) interior.X = interior.Right - interior.Width; else interior.Width = 10; } if (interior.Height < 10) { if (interior.Y + interior.Height > _cropMask.Exterior.Height) interior.Y = interior.Bottom - interior.Height; else interior.Height = 10; } _cropMask.Interior = interior; SetThumbs(_cropMask.Interior); RaiseEvent(new RoutedEventArgs(CropChangedEvent, this)); Keyboard.Focus(_cropMask); } //Cropping from the bottom-left. private void HandleBottomLeft(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(1, 0, -1, 1, args.HorizontalChange, args.VerticalChange); } //Cropping from the bottom-right. private void HandleBottomRight(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(0, 0, 1, 1, args.HorizontalChange, args.VerticalChange); } //Cropping from the top-right. private void HandleTopRight(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(0, 1, 1, -1, args.HorizontalChange, args.VerticalChange); } //Cropping from the top-left. private void HandleTopLeft(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(1, 1, -1, -1, args.HorizontalChange, args.VerticalChange); } //Cropping from the top. private void HandleTop(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(0, 1, 0, -1, args.HorizontalChange, args.VerticalChange); } //Cropping from the left. private void HandleLeft(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(1, 0, -1, 0, args.HorizontalChange, args.VerticalChange); } //Cropping from the right. private void HandleRight(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(0, 0, 1, 0, args.HorizontalChange, args.VerticalChange); } //Cropping from the bottom. private void HandleBottom(object sender, DragDeltaEventArgs args) { if (sender is Thumb) HandleThumb(0, 0, 0, 1, args.HorizontalChange, args.VerticalChange); } //Dragging the cropping selection. private void HandleCenter(object sender, DragDeltaEventArgs args) { if (!(sender is Thumb)) return; //Creates a new Rect based on the drag. var interior = new Rect(_cropMask.Interior.Left + args.HorizontalChange, _cropMask.Interior.Top + args.VerticalChange, _cropMask.Interior.Width, _cropMask.Interior.Height); #region Limit the drag to inside the bounds if (interior.Left < 0) interior.X = 0; if (interior.Top < 0) interior.Y = 0; if (interior.Right > _thumbCanvas.ActualWidth) interior.X = _thumbCanvas.ActualWidth - interior.Width; if (interior.Bottom > _thumbCanvas.ActualHeight) interior.Y = _thumbCanvas.ActualHeight - interior.Height; #endregion _cropMask.Interior = interior; SetThumbs(_cropMask.Interior); RaiseEvent(new RoutedEventArgs(CropChangedEvent, this)); Keyboard.Focus(_cropMask); } #endregion #region Other handlers private void AdornedElement_SizeChanged(object sender, SizeChangedEventArgs e) { if (!(sender is FrameworkElement element)) return; var wasChanged = false; double intLeft = ClipRectangle.Left, intTop = ClipRectangle.Top, intWidth = ClipRectangle.Width, intHeight = ClipRectangle.Height; if (ClipRectangle.Left > element.RenderSize.Width) { intLeft = element.RenderSize.Width; intWidth = 0; wasChanged = true; } if (ClipRectangle.Top > element.RenderSize.Height) { intTop = element.RenderSize.Height; intHeight = 0; wasChanged = true; } if (ClipRectangle.Right > element.RenderSize.Width) { intWidth = Math.Max(0, element.RenderSize.Width - intLeft); wasChanged = true; } if (ClipRectangle.Bottom > element.RenderSize.Height) { intHeight = Math.Max(0, element.RenderSize.Height - intTop); wasChanged = true; } if (wasChanged) ClipRectangle = new Rect(intLeft, intTop, intWidth, intHeight); } #endregion #region Arranging protected override Size ArrangeOverride(Size finalSize) { var rcExterior = new Rect(0, 0, AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height); _cropMask.Exterior = rcExterior; var rcInterior = _cropMask.Interior; _cropMask.Arrange(rcExterior); SetThumbs(rcInterior); _thumbCanvas.Arrange(rcExterior); return finalSize; } #endregion #region Public Methods public BitmapSource CropImage() { var margin = AdornerMargin(); var rcInterior = _cropMask.Interior; var pxFromSize = UnitsToPx(rcInterior.Width, rcInterior.Height); //CroppedBitmap indexes from the upper left of the margin whereas RenderTargetBitmap renders the //control exclusive of the margin. Hence our need to take the margins into account here... var pxFromPos = UnitsToPx(rcInterior.Left + margin.Left, rcInterior.Top + margin.Top); var pxWhole = UnitsToPx(AdornedElement.RenderSize.Width + margin.Left, AdornedElement.RenderSize.Height + margin.Left); pxFromSize.X = Math.Max(Math.Min(pxWhole.X - pxFromPos.X, pxFromSize.X), 0); pxFromSize.Y = Math.Max(Math.Min(pxWhole.Y - pxFromPos.Y, pxFromSize.Y), 0); if (pxFromSize.X == 0 || pxFromSize.Y == 0) return null; var rcFrom = new Int32Rect(pxFromPos.X, pxFromPos.Y, pxFromSize.X, pxFromSize.Y); var rtb = new RenderTargetBitmap(pxWhole.X, pxWhole.Y, DpiX, DpiY, PixelFormats.Default); rtb.Render(AdornedElement); return new CroppedBitmap(rtb, rcFrom); } #endregion #region Private Methods private void SetThumbs(Rect rc) { SetPosition(_thumbBottomRight, rc.Right, rc.Bottom); SetPosition(_thumbTopLeft, rc.Left, rc.Top); SetPosition(_thumbTopRight, rc.Right, rc.Top); SetPosition(_thumbBottomLeft, rc.Left, rc.Bottom); SetPosition(_thumbTop, rc.Left + rc.Width / 2, rc.Top); SetPosition(_thumbBottom, rc.Left + rc.Width / 2, rc.Bottom); SetPosition(_thumbLeft, rc.Left, rc.Top + rc.Height / 2); SetPosition(_thumbRight, rc.Right, rc.Top + rc.Height / 2); //Central thumb, used to drag the whole cropping selection. SetPosition(_thumbCenter, rc.Left + 5, rc.Top + 5); _thumbCenter.Width = rc.Right - rc.Left; _thumbCenter.Height = rc.Bottom - rc.Top; } private Thickness AdornerMargin() { var thick = new Thickness(0); if (AdornedElement is FrameworkElement element) thick = element.Margin; return thick; } private void BuildCorner(ref Thumb thumb, Cursor cursor) { if (thumb != null) return; thumb = new Thumb { Cursor = cursor, Style = (Style)FindResource("ScrollBar.Thumb"), Width = ThumbWidth, Height = ThumbWidth }; _thumbCanvas.Children.Add(thumb); } private void BuildCenter(ref Thumb thumb) { if (thumb != null) return; thumb = new Thumb { Style = (Style)FindResource("ThumbTranparent"), }; _thumbCanvas.Children.Add(thumb); } private Point UnitsToPx(double x, double y) { return new Point((int)(x * DpiX / 96), (int)(y * DpiY / 96)); } private void SetPosition(Thumb thumb, double x, double y) { Canvas.SetTop(thumb, y - ThumbWidth / 2); Canvas.SetLeft(thumb, x - ThumbWidth / 2); } #endregion #region Visual Tree Override // Override the VisualChildrenCount and GetVisualChild properties to interface with // the adorner's visual collection. protected override int VisualChildrenCount => _visualCollection.Count; protected override Visual GetVisualChild(int index) { return _visualCollection[index]; } #endregion } ================================================ FILE: ScreenToGif/Controls/DataGridHeaderBorder.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// A Border used to provide the default look of DataGrid headers. /// When Background or BorderBrush are set, the rendering will revert back to the default Border implementation. /// public sealed class DataGridHeaderBorder : Border { static DataGridHeaderBorder() { //We always set this to true on these borders, so just default it to true here. SnapsToDevicePixelsProperty.OverrideMetadata(typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(true)); } #region Header Appearance Properties /// /// Whether the hover look should be applied. /// public bool IsHovered { get => (bool)GetValue(IsHoveredProperty); set => SetValue(IsHoveredProperty, value); } /// /// DependencyProperty for IsHovered. /// public static readonly DependencyProperty IsHoveredProperty = DependencyProperty.Register("IsHovered", typeof(bool), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Whether the pressed look should be applied. /// public bool IsPressed { get => (bool)GetValue(IsPressedProperty); set => SetValue(IsPressedProperty, value); } /// /// DependencyProperty for IsPressed. /// public static readonly DependencyProperty IsPressedProperty = DependencyProperty.Register("IsPressed", typeof(bool), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsArrange)); /// /// When false, will not apply the hover look even when IsHovered is true. /// public bool IsClickable { get => (bool)GetValue(IsClickableProperty); set => SetValue(IsClickableProperty, value); } /// /// DependencyProperty for IsClickable. /// public static readonly DependencyProperty IsClickableProperty = DependencyProperty.Register("IsClickable", typeof(bool), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsArrange)); /// /// Whether to appear sorted. /// public ListSortDirection? SortDirection { get => (ListSortDirection?)GetValue(SortDirectionProperty); set => SetValue(SortDirectionProperty, value); } /// /// DependencyProperty for SortDirection. /// public static readonly DependencyProperty SortDirectionProperty = DependencyProperty.Register("SortDirection", typeof(ListSortDirection?), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Whether to appear selected. /// public bool IsSelected { get => (bool)GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } /// /// DependencyProperty for IsSelected. /// public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected", typeof(bool), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Vertical = column header /// Horizontal = row header /// public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } /// /// DependencyProperty for Orientation. /// public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsRender)); /// /// When there is a Background or BorderBrush, revert to the Border implementation. /// private bool UsingBorderImplementation => (Background != null) || (BorderBrush != null); /// /// Property that indicates the brush to use when drawing separators between headers. /// public Brush SeparatorBrush { get => (Brush)GetValue(SeparatorBrushProperty); set => SetValue(SeparatorBrushProperty, value); } /// /// DependencyProperty for SeparatorBrush. /// public static readonly DependencyProperty SeparatorBrushProperty = DependencyProperty.Register("SeparatorBrush", typeof(Brush), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(null)); /// /// Property that indicates the Visibility for the header separators. /// public Visibility SeparatorVisibility { get => (Visibility)GetValue(SeparatorVisibilityProperty); set => SetValue(SeparatorVisibilityProperty, value); } /// /// DependencyProperty for SeparatorBrush. /// public static readonly DependencyProperty SeparatorVisibilityProperty = DependencyProperty.Register("SeparatorVisibility", typeof(Visibility), typeof(DataGridHeaderBorder), new FrameworkPropertyMetadata(Visibility.Visible)); #endregion #region Layout /// /// Calculates the desired size of the element given the constraint. /// protected override Size MeasureOverride(Size constraint) { if (UsingBorderImplementation) { // Revert to the Border implementation return base.MeasureOverride(constraint); } var child = Child; if (child == null) return new Size(); // Use the public Padding property if it's set var padding = Padding; if (padding.Equals(new Thickness())) padding = DefaultPadding; var childWidth = constraint.Width; var childHeight = constraint.Height; // If there is an actual constraint, then reserve space for the chrome if (!double.IsInfinity(childWidth)) { childWidth = Math.Max(0.0, childWidth - padding.Left - padding.Right); } if (!double.IsInfinity(childHeight)) { childHeight = Math.Max(0.0, childHeight - padding.Top - padding.Bottom); } child.Measure(new Size(childWidth, childHeight)); var desiredSize = child.DesiredSize; // Add on the reserved space for the chrome return new Size(desiredSize.Width + padding.Left + padding.Right, desiredSize.Height + padding.Top + padding.Bottom); } /// /// Positions children and returns the final size of the element. /// protected override Size ArrangeOverride(Size arrangeSize) { if (UsingBorderImplementation) { // Revert to the Border implementation return base.ArrangeOverride(arrangeSize); } var child = Child; if (child != null) { // Use the public Padding property if it's set var padding = Padding; if (padding.Equals(new Thickness())) { padding = DefaultPadding; } // Reserve space for the chrome var childWidth = Math.Max(0.0, arrangeSize.Width - padding.Left - padding.Right); var childHeight = Math.Max(0.0, arrangeSize.Height - padding.Top - padding.Bottom); child.Arrange(new Rect(padding.Left, padding.Top, childWidth, childHeight)); } return arrangeSize; } #endregion #region Rendering /// /// Returns a default padding for the various themes for use /// by measure and arrange. /// private Thickness DefaultPadding { get { var padding = new Thickness(3.0); // The default padding var themePadding = ThemeDefaultPadding; if (themePadding == null) { if (Orientation == Orientation.Vertical) { // Reserve space to the right for the arrow padding.Right = 15.0; } } else { padding = (Thickness)themePadding; } // When pressed, offset the child if (IsPressed && IsClickable) { padding.Left += 1.0; padding.Top += 1.0; padding.Right -= 1.0; padding.Bottom -= 1.0; } return padding; } } /// /// Called when this element should re-render. /// protected override void OnRender(DrawingContext dc) { if (UsingBorderImplementation) { // Revert to the Border implementation base.OnRender(dc); } else { RenderTheme(dc); } } private static double Max0(double d) { return Math.Max(0.0, d); } #endregion #region Freezable Cache /// /// Creates a cache of frozen Freezable resources for use across all instances of the border. /// private static void EnsureCache(int size) { // Quick check to avoid locking if (_freezableCache == null) { lock (_cacheAccess) { // Re-check in case another thread created the cache if (_freezableCache == null) { _freezableCache = new List(size); for (var i = 0; i < size; i++) { _freezableCache.Add(null); } } } } Debug.Assert(_freezableCache.Count == size, "The cache size does not match the requested amount."); } /// /// Releases all resources in the cache. /// private static void ReleaseCache() { // Avoid locking if necessary if (_freezableCache != null) { lock (_cacheAccess) { // No need to re-check if non-null since it's OK to set it to null multiple times _freezableCache = null; } } } /// /// Retrieves a cached resource. /// private static Freezable GetCachedFreezable(int index) { lock (_cacheAccess) { var freezable = _freezableCache[index]; Debug.Assert((freezable == null) || freezable.IsFrozen, "Cached Freezables should have been frozen."); return freezable; } } /// /// Caches a resources. /// private static void CacheFreezable(Freezable freezable, int index) { Debug.Assert(freezable.IsFrozen, "Cached Freezables should be frozen."); lock (_cacheAccess) { if (_freezableCache[index] != null) { _freezableCache[index] = freezable; } } } private static List _freezableCache; private static readonly object _cacheAccess = new object(); #endregion #region Theme Rendering private Thickness? ThemeDefaultPadding { get { if (Orientation == Orientation.Vertical) { return new Thickness(5.0, 4.0, 5.0, 4.0); } return null; } } private void RenderTheme(DrawingContext dc) { var size = RenderSize; var horizontal = Orientation == Orientation.Horizontal; var isClickable = IsClickable && IsEnabled; var isHovered = isClickable && IsHovered; var isPressed = isClickable && IsPressed; var sortDirection = SortDirection; var isSorted = sortDirection != null; var isSelected = IsSelected; var hasBevel = (!isHovered && !isPressed && !isSorted && !isSelected); EnsureCache((int)AeroFreezables.NumFreezables); if (horizontal) { // When horizontal, rotate the rendering by -90 degrees var m1 = new Matrix(); m1.RotateAt(-90.0, 0.0, 0.0); var m2 = new Matrix(); m2.Translate(0.0, size.Height); var horizontalRotate = new MatrixTransform(m1 * m2); horizontalRotate.Freeze(); dc.PushTransform(horizontalRotate); var temp = size.Width; size.Width = size.Height; size.Height = temp; } if (hasBevel) { // This is a highlight that can be drawn by just filling the background with the color. // It will be seen through the gab between the border and the background. var bevel = (LinearGradientBrush)GetCachedFreezable((int)AeroFreezables.NormalBevel); if (bevel == null) { bevel = new LinearGradientBrush(); bevel.StartPoint = new Point(); bevel.EndPoint = new Point(0.0, 1.0); bevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0)); bevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.4)); bevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFC, 0xFC, 0xFD), 0.4)); bevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFB, 0xFC, 0xFC), 1.0)); bevel.Freeze(); CacheFreezable(bevel, (int)AeroFreezables.NormalBevel); } dc.DrawRectangle(bevel, null, new Rect(0.0, 0.0, size.Width, size.Height)); } // Fill the background var backgroundType = AeroFreezables.NormalBackground; if (isPressed) { backgroundType = AeroFreezables.PressedBackground; } else if (isHovered) { backgroundType = AeroFreezables.HoveredBackground; } else if (isSorted || isSelected) { backgroundType = AeroFreezables.SortedBackground; } var background = (LinearGradientBrush)GetCachedFreezable((int)backgroundType); if (background == null) { background = new LinearGradientBrush(); background.StartPoint = new Point(); background.EndPoint = new Point(0.0, 1.0); switch (backgroundType) { case AeroFreezables.NormalBackground: background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xF7, 0xF8, 0xFA), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xF1, 0xF2, 0xF4), 1.0)); break; case AeroFreezables.PressedBackground: background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xBC, 0xE4, 0xF9), 0.0)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xBC, 0xE4, 0xF9), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x8D, 0xD6, 0xF7), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x8A, 0xD1, 0xF5), 1.0)); break; case AeroFreezables.HoveredBackground: background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xE3, 0xF7, 0xFF), 0.0)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xE3, 0xF7, 0xFF), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xBD, 0xED, 0xFF), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xB7, 0xE7, 0xFB), 1.0)); break; case AeroFreezables.SortedBackground: background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xF2, 0xF9, 0xFC), 0.0)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xF2, 0xF9, 0xFC), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xE1, 0xF1, 0xF9), 0.4)); background.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xD8, 0xEC, 0xF6), 1.0)); break; } background.Freeze(); CacheFreezable(background, (int)backgroundType); } dc.DrawRectangle(background, null, new Rect(0.0, 0.0, size.Width, size.Height)); if (size.Width >= 2.0) { // Draw the borders on the sides var sideType = AeroFreezables.NormalSides; if (isPressed) { sideType = AeroFreezables.PressedSides; } else if (isHovered) { sideType = AeroFreezables.HoveredSides; } else if (isSorted || isSelected) { sideType = AeroFreezables.SortedSides; } if (SeparatorVisibility == Visibility.Visible) { Brush sideBrush; if (SeparatorBrush != null) { sideBrush = SeparatorBrush; } else { sideBrush = (Brush)GetCachedFreezable((int)sideType); if (sideBrush == null) { LinearGradientBrush lgBrush = null; if (sideType != AeroFreezables.SortedSides) { lgBrush = new LinearGradientBrush(); lgBrush.StartPoint = new Point(); lgBrush.EndPoint = new Point(0.0, 1.0); sideBrush = lgBrush; } switch (sideType) { case AeroFreezables.NormalSides: lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xF2, 0xF2, 0xF2), 0.0)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xEF, 0xEF, 0xEF), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xE7, 0xE8, 0xEA), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xDE, 0xDF, 0xE1), 1.0)); break; case AeroFreezables.PressedSides: lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x7A, 0x9E, 0xB1), 0.0)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x7A, 0x9E, 0xB1), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x50, 0x91, 0xAF), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x4D, 0x8D, 0xAD), 1.0)); break; case AeroFreezables.HoveredSides: lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x88, 0xCB, 0xEB), 0.0)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x88, 0xCB, 0xEB), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x69, 0xBB, 0xE3), 0.4)); lgBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x69, 0xBB, 0xE3), 1.0)); break; case AeroFreezables.SortedSides: sideBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x96, 0xD9, 0xF9)); break; } sideBrush.Freeze(); CacheFreezable(sideBrush, (int)sideType); } } dc.DrawRectangle(sideBrush, null, new Rect(0.0, 0.0, 1.0, Max0(size.Height - 0.95))); dc.DrawRectangle(sideBrush, null, new Rect(size.Width - 1.0, 0.0, 1.0, Max0(size.Height - 0.95))); } } if (isPressed && (size.Width >= 4.0) && (size.Height >= 4.0)) { // When pressed, there are added borders on the left and top var topBrush = (LinearGradientBrush)GetCachedFreezable((int)AeroFreezables.PressedTop); if (topBrush == null) { topBrush = new LinearGradientBrush(); topBrush.StartPoint = new Point(); topBrush.EndPoint = new Point(0.0, 1.0); topBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x86, 0xA3, 0xB2), 0.0)); topBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x86, 0xA3, 0xB2), 0.1)); topBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xAA, 0xCE, 0xE1), 0.9)); topBrush.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xAA, 0xCE, 0xE1), 1.0)); topBrush.Freeze(); CacheFreezable(topBrush, (int)AeroFreezables.PressedTop); } dc.DrawRectangle(topBrush, null, new Rect(0.0, 0.0, size.Width, 2.0)); var pressedBevel = (LinearGradientBrush)GetCachedFreezable((int)AeroFreezables.PressedBevel); if (pressedBevel == null) { pressedBevel = new LinearGradientBrush(); pressedBevel.StartPoint = new Point(); pressedBevel.EndPoint = new Point(0.0, 1.0); pressedBevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xA2, 0xCB, 0xE0), 0.0)); pressedBevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xA2, 0xCB, 0xE0), 0.4)); pressedBevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x72, 0xBC, 0xDF), 0.4)); pressedBevel.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x6E, 0xB8, 0xDC), 1.0)); pressedBevel.Freeze(); CacheFreezable(pressedBevel, (int)AeroFreezables.PressedBevel); } dc.DrawRectangle(pressedBevel, null, new Rect(1.0, 0.0, 1.0, size.Height - 0.95)); dc.DrawRectangle(pressedBevel, null, new Rect(size.Width - 2.0, 0.0, 1.0, size.Height - 0.95)); } if (size.Height >= 2.0) { // Draw the bottom border var bottomType = AeroFreezables.NormalBottom; if (isPressed) { bottomType = AeroFreezables.PressedOrHoveredBottom; } else if (isHovered) { bottomType = AeroFreezables.PressedOrHoveredBottom; } else if (isSorted || isSelected) { bottomType = AeroFreezables.SortedBottom; } var bottomBrush = (SolidColorBrush)GetCachedFreezable((int)bottomType); if (bottomBrush == null) { switch (bottomType) { case AeroFreezables.NormalBottom: bottomBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0xD5, 0xD5, 0xD5)); break; case AeroFreezables.PressedOrHoveredBottom: bottomBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x93, 0xC9, 0xE3)); break; case AeroFreezables.SortedBottom: bottomBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x96, 0xD9, 0xF9)); break; } bottomBrush.Freeze(); CacheFreezable(bottomBrush, (int)bottomType); } dc.DrawRectangle(bottomBrush, null, new Rect(0.0, size.Height - 1.0, size.Width, 1.0)); } if (isSorted && (size.Width > 14.0) && (size.Height > 10.0)) { // Draw the sort arrow var positionTransform = new TranslateTransform((size.Width - 8.0) * 0.5, 1.0); positionTransform.Freeze(); dc.PushTransform(positionTransform); var ascending = (sortDirection == ListSortDirection.Ascending); var arrowGeometry = (PathGeometry)GetCachedFreezable(ascending ? (int)AeroFreezables.ArrowUpGeometry : (int)AeroFreezables.ArrowDownGeometry); if (arrowGeometry == null) { arrowGeometry = new PathGeometry(); var arrowFigure = new PathFigure(); if (ascending) { arrowFigure.StartPoint = new Point(0.0, 4.0); var line = new LineSegment(new Point(4.0, 0.0), false); line.Freeze(); arrowFigure.Segments.Add(line); line = new LineSegment(new Point(8.0, 4.0), false); line.Freeze(); arrowFigure.Segments.Add(line); } else { arrowFigure.StartPoint = new Point(0.0, 0.0); var line = new LineSegment(new Point(8.0, 0.0), false); line.Freeze(); arrowFigure.Segments.Add(line); line = new LineSegment(new Point(4.0, 4.0), false); line.Freeze(); arrowFigure.Segments.Add(line); } arrowFigure.IsClosed = true; arrowFigure.Freeze(); arrowGeometry.Figures.Add(arrowFigure); arrowGeometry.Freeze(); CacheFreezable(arrowGeometry, ascending ? (int)AeroFreezables.ArrowUpGeometry : (int)AeroFreezables.ArrowDownGeometry); } // Draw two arrows, one inset in the other. This is to achieve a double gradient over both the border and the fill. var arrowBorder = (LinearGradientBrush)GetCachedFreezable((int)AeroFreezables.ArrowBorder); if (arrowBorder == null) { arrowBorder = new LinearGradientBrush(); arrowBorder.StartPoint = new Point(); arrowBorder.EndPoint = new Point(1.0, 1.0); arrowBorder.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x3C, 0x5E, 0x72), 0.0)); arrowBorder.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x3C, 0x5E, 0x72), 0.1)); arrowBorder.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xC3, 0xE4, 0xF5), 1.0)); arrowBorder.Freeze(); CacheFreezable(arrowBorder, (int)AeroFreezables.ArrowBorder); } dc.DrawGeometry(arrowBorder, null, arrowGeometry); var arrowFill = (LinearGradientBrush)GetCachedFreezable((int)AeroFreezables.ArrowFill); if (arrowFill == null) { arrowFill = new LinearGradientBrush(); arrowFill.StartPoint = new Point(); arrowFill.EndPoint = new Point(1.0, 1.0); arrowFill.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x61, 0x96, 0xB6), 0.0)); arrowFill.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0x61, 0x96, 0xB6), 0.1)); arrowFill.GradientStops.Add(new GradientStop(Color.FromArgb(0xFF, 0xCA, 0xE6, 0xF5), 1.0)); arrowFill.Freeze(); CacheFreezable(arrowFill, (int)AeroFreezables.ArrowFill); } // Inset the fill arrow inside the border arrow var arrowScale = (ScaleTransform)GetCachedFreezable((int)AeroFreezables.ArrowFillScale); if (arrowScale == null) { arrowScale = new ScaleTransform(0.75, 0.75, 3.5, 4.0); arrowScale.Freeze(); CacheFreezable(arrowScale, (int)AeroFreezables.ArrowFillScale); } dc.PushTransform(arrowScale); dc.DrawGeometry(arrowFill, null, arrowGeometry); dc.Pop(); // Scale Transform dc.Pop(); // Position Transform } if (horizontal) { dc.Pop(); // Horizontal Rotate } } private enum AeroFreezables : int { NormalBevel, NormalBackground, PressedBackground, HoveredBackground, SortedBackground, PressedTop, NormalSides, PressedSides, HoveredSides, SortedSides, PressedBevel, NormalBottom, PressedOrHoveredBottom, SortedBottom, ArrowBorder, ArrowFill, ArrowFillScale, ArrowUpGeometry, ArrowDownGeometry, NumFreezables } #endregion } ================================================ FILE: ScreenToGif/Controls/DecimalBox.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; public class DecimalBox : ExtendedTextBox { #region Variables private bool _ignore; private string _baseFormat = "{0:###,###,###,###,##0."; private string _format = "{0:###,###,###,###,##0.00}"; #endregion #region Dependency Property public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(decimal.MaxValue, OnMaximumPropertyChanged)); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(0M, OnValuePropertyChanged)); public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(0M, OnMinimumPropertyChanged)); public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(nameof(Decimals), typeof(int), typeof(DecimalBox), new FrameworkPropertyMetadata(2, OnDecimalsPropertyChanged)); public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(StepValue), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(1M)); public static readonly DependencyProperty UpdateOnInputProperty = DependencyProperty.Register(nameof(UpdateOnInput), typeof(bool), typeof(DecimalBox), new FrameworkPropertyMetadata(false, OnUpdateOnInputPropertyChanged)); public static readonly DependencyProperty DefaultValueIfEmptyProperty = DependencyProperty.Register(nameof(DefaultValueIfEmpty), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(0M)); public static readonly DependencyProperty EmptyIfValueProperty = DependencyProperty.Register(nameof(EmptyIfValue), typeof(decimal), typeof(DecimalBox), new FrameworkPropertyMetadata(decimal.MinValue)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(decimal), typeof(DecimalBox), new PropertyMetadata(1M, OnScalePropertyChanged)); #endregion #region Properties [Bindable(true), Category("Common")] public decimal Maximum { get => (decimal)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } [Bindable(true), Category("Common")] public decimal Value { get => (decimal)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } [Bindable(true), Category("Common")] public decimal Minimum { get => (decimal)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } [Bindable(true), Category("Common")] public int Decimals { get => (int)GetValue(DecimalsProperty); set => SetValue(DecimalsProperty, value); } /// /// The Increment/Decrement value. /// [Description("The Increment/Decrement value.")] public decimal StepValue { get => (decimal)GetValue(StepProperty); set => SetValue(StepProperty, value); } [Bindable(true), Category("Common")] public bool UpdateOnInput { get => (bool)GetValue(UpdateOnInputProperty); set => SetValue(UpdateOnInputProperty, value); } [Bindable(true), Category("Common")] public decimal DefaultValueIfEmpty { get => (decimal)GetValue(DefaultValueIfEmptyProperty); set => SetValue(DefaultValueIfEmptyProperty, value); } [Bindable(true), Category("Common")] public decimal EmptyIfValue { get => (decimal)GetValue(EmptyIfValueProperty); set => SetValue(EmptyIfValueProperty, value); } [Bindable(true), Category("Common")] public decimal Scale { get => (decimal)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } #endregion #region Properties Changed private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DecimalBox decimalBox)) return; if (decimalBox.Value > decimalBox.Maximum) decimalBox.Value = decimalBox.Maximum; } private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DecimalBox decimalBox)) return; if (decimalBox.Value > decimalBox.Maximum) decimalBox.Value = decimalBox.Maximum; else if (decimalBox.Value < decimalBox.Minimum) decimalBox.Value = decimalBox.Minimum; decimalBox.Value = Math.Round(decimalBox.Value, decimalBox.Decimals); if (!decimalBox._ignore) { var value = string.Format(CultureInfo.CurrentCulture, decimalBox._format, decimalBox.Value * decimalBox.Scale); if (!string.Equals(decimalBox.Text, value)) decimalBox.Text = (decimalBox.EmptyIfValue == decimalBox.Value ? "" : value); } decimalBox.RaiseValueChangedEvent(); } private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DecimalBox decimalBox)) return; if (decimalBox.Value < decimalBox.Minimum) decimalBox.Value = decimalBox.Minimum; } private static void OnDecimalsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DecimalBox decimalBox)) return; decimalBox._format = decimalBox._baseFormat + "".PadRight(decimalBox.Decimals, '0') + "}"; decimalBox.Value = Math.Round(decimalBox.Value, decimalBox.Decimals); } private static void OnUpdateOnInputPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DecimalBox)d).UpdateOnInput = (bool)e.NewValue; } private static void OnScalePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DecimalBox decimalBox)) return; //The scale value dictates the value being displayed. //For example, The value 600 and the scale 1.25 should display the text 750. //Text = Value * Scale. var scaled = decimalBox.Value * decimalBox.Scale; decimalBox.Text = scaled == decimalBox.EmptyIfValue ? "" : string.Format(CultureInfo.CurrentCulture, decimalBox._format, decimalBox.Value * decimalBox.Scale); } #endregion #region Custom Events public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DecimalBox)); /// /// Event raised when the numeric value is changed. /// public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } public void RaiseValueChangedEvent() { if (ValueChangedEvent == null) return; RaiseEvent(new RoutedEventArgs(ValueChangedEvent)); } #endregion static DecimalBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DecimalBox), new FrameworkPropertyMetadata(typeof(DecimalBox))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); _format = _baseFormat + "".PadRight(Decimals, '0') + "}"; } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); Text = Value == EmptyIfValue ? "" : string.Format(CultureInfo.CurrentCulture, _format, Value); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Only sets the focus if not clicking on the Up/Down buttons of a IntegerUpDown. if (e.OriginalSource is TextBlock || e.OriginalSource is Border) return; if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (e.Source is DecimalBox) SelectAll(); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (string.IsNullOrEmpty(e.Text)) { e.Handled = true; return; } if (!IsEntryAllowed(this, e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnTextChanged(TextChangedEventArgs e) { if (!UpdateOnInput || _ignore || string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) return; _ignore = true; Value = Math.Round(Convert.ToDecimal(Text, CultureInfo.CurrentCulture) / Scale, Decimals); _ignore = false; base.OnTextChanged(e); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (!UpdateOnInput) { if (string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) { Value = DefaultValueIfEmpty; return; } _ignore = true; Value = Convert.ToDecimal(Text, CultureInfo.CurrentCulture); Text = EmptyIfValue == Value ? "" : string.Format(CultureInfo.CurrentCulture, _format, Value); _ignore = false; return; } Text = Value == EmptyIfValue ? "" : string.Format(CultureInfo.CurrentCulture, _format, Value); } protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Enter || e.Key == Key.Return) { e.Handled = true; MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } base.OnKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); if (!IsKeyboardFocusWithin) return; var step = Keyboard.Modifiers == (ModifierKeys.Shift | ModifierKeys.Control) ? 50 : Keyboard.Modifiers == ModifierKeys.Shift ? 10 : Keyboard.Modifiers == ModifierKeys.Control ? 5 : StepValue; if (e.Delta > 0) Value += step; else Value -= step; e.Handled = true; } #endregion #region Base Properties Changed private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!IsTextAllowed(text)) e.CancelCommand(); } else { e.CancelCommand(); } } #endregion #region Methods private bool IsEntryAllowed(TextBox textBox, string text) { //Digits, points or commas. var regex = new Regex(@"^[0-9]|\.|\,$"); //TODO: Support for multiple cultures. //Checks if it's a valid char based on the context. return regex.IsMatch(text) && IsEntryAllowedInContext(textBox, text); } private bool IsEntryAllowedInContext(TextBox textBox, string next) { //if number, allow. if (char.IsNumber(next.ToCharArray().FirstOrDefault())) return true; #region Thousands var thousands = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; var thousandsChar = thousands.ToCharArray().FirstOrDefault(); var decimals = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; var decimalsChar = decimals.ToCharArray().FirstOrDefault(); if (next.Equals(thousands)) { var textAux = textBox.Text; if (!string.IsNullOrEmpty(textBox.SelectedText)) textAux = textAux.Replace(textBox.SelectedText, ""); var before = textAux.Substring(0, textBox.SelectionStart); var after = textAux.Substring(textBox.SelectionStart); //If there's no text, is not allowed to add a thousand separator. if (string.IsNullOrEmpty(after + before)) return false; //Before the carret. if (!string.IsNullOrEmpty(before)) { //You can't add a thousand separator after the decimal. if (before.Contains(decimals)) return false; //Check the previous usage of a thousand separator. if (before.Contains(thousands)) { var split = before.Split(thousandsChar); //You can't add a thousand separators closer than 3 chars from each other. if (split.Last().Length != 3) return false; } } //After the carret. if (!string.IsNullOrEmpty(after)) { var split = after.Split(thousandsChar, decimalsChar); //You can't add a thousand separators closer than 3 chars from another separator, decimal or thousands. if (split.First().Length != 3) return true; } return false; } #endregion #region Decimal if (next.Equals(decimals)) return !textBox.Text.Any(x => x.Equals(decimalsChar)); #endregion return true; } private bool IsTextAllowed(string text) { return decimal.TryParse(text, out decimal _); //var regex = new Regex(@"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$", RegexOptions.CultureInvariant); //return regex.IsMatch(text); } #endregion } ================================================ FILE: ScreenToGif/Controls/DecimalUpDown.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; namespace ScreenToGif.Controls; /// /// Decimal only control with up and down buttons to change the value. /// public class DecimalUpDown : DecimalBox { #region Variables private RepeatButton _upButton; private RepeatButton _downButton; #endregion static DecimalUpDown() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DecimalUpDown), new FrameworkPropertyMetadata(typeof(DecimalUpDown))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); //Internal controls. _upButton = Template.FindName("UpButton", this) as RepeatButton; _downButton = Template.FindName("DownButton", this) as RepeatButton; if (_upButton != null) _upButton.Click += UpButton_Click; if (_downButton != null) _downButton.Click += DownButton_Click; } #region Event Handlers private void DownButton_Click(object sender, RoutedEventArgs e) { if (Value > Minimum) Value -= StepValue; } private void UpButton_Click(object sender, RoutedEventArgs e) { if (Value < Maximum) Value += StepValue; } #endregion } ================================================ FILE: ScreenToGif/Controls/DisplayTimer.cs ================================================ using System; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; namespace ScreenToGif.Controls; public class DisplayTimer : Control { private DispatcherTimer _timer = null; private Stopwatch _watch = null; public static readonly DependencyPropertyKey ElapsedPropertyKey = DependencyProperty.RegisterReadOnly(nameof(Elapsed), typeof(TimeSpan), typeof(DisplayTimer), new PropertyMetadata(TimeSpan.Zero)); public static readonly DependencyProperty ElapsedProperty = ElapsedPropertyKey.DependencyProperty; public static readonly DependencyPropertyKey IsRunningPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsRunning), typeof(bool), typeof(DisplayTimer), new PropertyMetadata(default(bool))); public static readonly DependencyProperty IsRunningProperty = IsRunningPropertyKey.DependencyProperty; public static readonly DependencyPropertyKey IsPausedPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPaused), typeof(bool), typeof(DisplayTimer), new PropertyMetadata(default(bool))); public static readonly DependencyProperty IsPausedProperty = IsPausedPropertyKey.DependencyProperty; public static readonly DependencyPropertyKey IsNegativePropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsNegative), typeof(bool), typeof(DisplayTimer), new PropertyMetadata(default(bool))); public static readonly DependencyProperty IsNegativeProperty = IsNegativePropertyKey.DependencyProperty; public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(DisplayTimer), new PropertyMetadata(default(CornerRadius))); public static readonly DependencyProperty CapturedCountProperty = DependencyProperty.Register(nameof(CapturedCount), typeof(int), typeof(DisplayTimer), new PropertyMetadata(0)); public static readonly DependencyProperty ManuallyCapturedCountProperty = DependencyProperty.Register(nameof(ManuallyCapturedCount), typeof(int), typeof(DisplayTimer), new PropertyMetadata(0)); public static readonly DependencyProperty IsImpreciseCaptureProperty = DependencyProperty.Register(nameof(IsImpreciseCapture), typeof(bool), typeof(DisplayTimer), new PropertyMetadata(false)); public TimeSpan Elapsed { get => (TimeSpan)GetValue(ElapsedProperty); protected set => SetValue(ElapsedPropertyKey, value); } public bool IsRunning { get => (bool)GetValue(IsRunningProperty); protected set => SetValue(IsRunningPropertyKey, value); } public bool IsPaused { get => (bool)GetValue(IsPausedProperty); protected set => SetValue(IsPausedPropertyKey, value); } public bool IsNegative { get => (bool)GetValue(IsNegativeProperty); protected set => SetValue(IsNegativePropertyKey, value); } public CornerRadius CornerRadius { get => (CornerRadius)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } public int CapturedCount { get => (int)GetValue(CapturedCountProperty); set => SetValue(CapturedCountProperty, value); } public int ManuallyCapturedCount { get => (int)GetValue(ManuallyCapturedCountProperty); set => SetValue(ManuallyCapturedCountProperty, value); } public bool IsImpreciseCapture { get => (bool)GetValue(IsImpreciseCaptureProperty); set => SetValue(IsImpreciseCaptureProperty, value); } static DisplayTimer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DisplayTimer), new FrameworkPropertyMetadata(typeof(DisplayTimer))); } ~DisplayTimer() { _timer?.Stop(); } private void SyncElapsed() { Elapsed = _watch.Elapsed; } public void Start() { IsRunning = true; IsPaused = false; IsNegative = false; if (_timer != null) { _watch.Start(); _timer.Start(); return; } _watch = new Stopwatch(); _timer = new DispatcherTimer(new TimeSpan(0, 0, 0, 0, 100), DispatcherPriority.Background, (sender, args) => { SyncElapsed(); }, Dispatcher.CurrentDispatcher); SyncElapsed(); _watch.Start(); _timer.Start(); } public void Pause() { if (!IsRunning) return; _watch.Stop(); _timer.Stop(); IsRunning = false; IsPaused = true; } public void Stop() { _watch?.Stop(); _timer?.Stop(); _watch = null; _timer = null; ManuallyCapturedCount = 0; Elapsed = TimeSpan.Zero; IsRunning = false; IsPaused = false; } public void Reset() { _watch.Stop(); _timer?.Stop(); IsRunning = false; IsPaused = false; ManuallyCapturedCount = 0; Elapsed = TimeSpan.Zero; Start(); } public void SetElapsed(int seconds) { if (IsRunning) return; Elapsed = new TimeSpan(0, 0, 0, seconds); IsNegative = Elapsed < TimeSpan.Zero; } } ================================================ FILE: ScreenToGif/Controls/DoubleBox.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; public class DoubleBox : ExtendedTextBox { #region Variables private bool _ignore; private string _baseFormat = "{0:###,###,###,###,##0."; private string _format = "{0:###,###,###,###,##0.00}"; #endregion #region Dependency Property public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(DoubleBox), new FrameworkPropertyMetadata(double.MaxValue, OnMaximumPropertyChanged)); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(double), typeof(DoubleBox), new FrameworkPropertyMetadata(0D, OnValuePropertyChanged)); public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(DoubleBox), new FrameworkPropertyMetadata(0D, OnMinimumPropertyChanged)); public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(nameof(Decimals), typeof(int), typeof(DoubleBox), new FrameworkPropertyMetadata(2, OnDecimalsPropertyChanged)); public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(StepValue), typeof(double), typeof(DoubleBox), new FrameworkPropertyMetadata(1D)); public static readonly DependencyProperty UpdateOnInputProperty = DependencyProperty.Register(nameof(UpdateOnInput), typeof(bool), typeof(DoubleBox), new FrameworkPropertyMetadata(false, OnUpdateOnInputPropertyChanged)); public static readonly DependencyProperty DefaultValueIfEmptyProperty = DependencyProperty.Register(nameof(DefaultValueIfEmpty), typeof(double), typeof(DoubleBox), new FrameworkPropertyMetadata(0D)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(double), typeof(DoubleBox), new PropertyMetadata(1D, OnScalePropertyChanged)); #endregion #region Properties [Bindable(true), Category("Common")] public double Maximum { get => (double)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } [Bindable(true), Category("Common")] public double Value { get => (double)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } [Bindable(true), Category("Common")] public double Minimum { get => (double)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } [Bindable(true), Category("Common")] public int Decimals { get => (int)GetValue(DecimalsProperty); set => SetValue(DecimalsProperty, value); } /// /// The Increment/Decrement value. /// [Description("The Increment/Decrement value.")] public double StepValue { get => (double)GetValue(StepProperty); set => SetValue(StepProperty, value); } [Bindable(true), Category("Common")] public bool UpdateOnInput { get => (bool)GetValue(UpdateOnInputProperty); set => SetValue(UpdateOnInputProperty, value); } [Bindable(true), Category("Common")] public double DefaultValueIfEmpty { get => (double)GetValue(DefaultValueIfEmptyProperty); set => SetValue(DefaultValueIfEmptyProperty, value); } [Bindable(true), Category("Common")] public double Scale { get => (double)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } #endregion #region Properties Changed private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DoubleBox doubleBox)) return; if (doubleBox.Value > doubleBox.Maximum) doubleBox.Value = doubleBox.Maximum; } private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DoubleBox doubleBox)) return; if (doubleBox.Value > doubleBox.Maximum) doubleBox.Value = doubleBox.Maximum; else if (doubleBox.Value < doubleBox.Minimum) doubleBox.Value = doubleBox.Minimum; doubleBox.Value = Math.Round(doubleBox.Value, doubleBox.Decimals); if (!doubleBox._ignore) { var value = string.Format(CultureInfo.CurrentCulture, doubleBox._format, doubleBox.Value * doubleBox.Scale); if (!string.Equals(doubleBox.Text, value)) doubleBox.Text = value; } doubleBox.RaiseValueChangedEvent(); } private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DoubleBox doubleBox)) return; if (doubleBox.Value < doubleBox.Minimum) doubleBox.Value = doubleBox.Minimum; } private static void OnDecimalsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DoubleBox doubleBox)) return; doubleBox._format = doubleBox._baseFormat + "".PadRight(doubleBox.Decimals, '0') + "}"; doubleBox.Value = Math.Round(doubleBox.Value, doubleBox.Decimals); } private static void OnUpdateOnInputPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DoubleBox)d).UpdateOnInput = (bool)e.NewValue; } private static void OnScalePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DoubleBox doubleBox)) return; //The scale value dictates the value being displayed. //For example, The value 600 and the scale 1.25 should display the text 750. //Text = Value * Scale. doubleBox.Text = string.Format(CultureInfo.CurrentCulture, doubleBox._format, doubleBox.Value * doubleBox.Scale); } #endregion #region Custom Events public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DoubleBox)); /// /// Event raised when the numeric value is changed. /// public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } public void RaiseValueChangedEvent() { if (ValueChangedEvent == null) return; RaiseEvent(new RoutedEventArgs(ValueChangedEvent)); } #endregion static DoubleBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DoubleBox), new FrameworkPropertyMetadata(typeof(DoubleBox))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); _format = _baseFormat + "".PadRight(Decimals, '0') + "}"; } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); Text = string.Format(CultureInfo.CurrentCulture, _format, Value); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Only sets the focus if not clicking on the Up/Down buttons of a IntegerUpDown. if (e.OriginalSource is TextBlock || e.OriginalSource is Border) return; if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (e.Source is DoubleBox) SelectAll(); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (string.IsNullOrEmpty(e.Text)) { e.Handled = true; return; } if (!IsEntryAllowed(this, e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnTextChanged(TextChangedEventArgs e) { if (!UpdateOnInput || _ignore) return; if (string.IsNullOrEmpty(Text)) return; if (!IsTextAllowed(Text)) return; _ignore = true; Value = Math.Round(Convert.ToDouble(Text, CultureInfo.CurrentCulture) / Scale, Decimals); _ignore = false; base.OnTextChanged(e); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (!UpdateOnInput) { if (string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) { Value = DefaultValueIfEmpty; return; } _ignore = true; Value = Convert.ToDouble(Text, CultureInfo.CurrentCulture); Text = string.Format(CultureInfo.CurrentCulture, _format, Value); _ignore = false; return; } Text = string.Format(CultureInfo.CurrentCulture, _format, Value); } protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Enter || e.Key == Key.Return) { e.Handled = true; MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } base.OnKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); if (!IsKeyboardFocusWithin) return; var step = Keyboard.Modifiers == (ModifierKeys.Shift | ModifierKeys.Control) ? 50 : Keyboard.Modifiers == ModifierKeys.Shift ? 10 : Keyboard.Modifiers == ModifierKeys.Control ? 5 : StepValue; if (e.Delta > 0) Value += step; else Value -= step; e.Handled = true; } #endregion #region Base Properties Changed private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!IsTextAllowed(text)) e.CancelCommand(); } else { e.CancelCommand(); } } #endregion #region Methods private bool IsEntryAllowed(TextBox textBox, string text) { //Digits, points or commas. var regex = new Regex(@"^[0-9]|\.|\,$"); //TODO: Support for multiple cultures. //Checks if it's a valid char based on the context. return regex.IsMatch(text) && IsEntryAllowedInContext(textBox, text); } private bool IsEntryAllowedInContext(TextBox textBox, string next) { //if number, allow. if (char.IsNumber(next.ToCharArray().FirstOrDefault())) return true; #region Thousands var thousands = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; var thousandsChar = thousands.ToCharArray().FirstOrDefault(); var decimals = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; var decimalsChar = decimals.ToCharArray().FirstOrDefault(); if (next.Equals(thousands)) { var textAux = textBox.Text; if (!string.IsNullOrEmpty(textBox.SelectedText)) textAux = textAux.Replace(textBox.SelectedText, ""); var before = textAux.Substring(0, textBox.SelectionStart); var after = textAux.Substring(textBox.SelectionStart); //If there's no text, is not allowed to add a thousand separator. if (string.IsNullOrEmpty(after + before)) return false; //Before the carret. if (!string.IsNullOrEmpty(before)) { //You can't add a thousand separator after the decimal. if (before.Contains(decimals)) return false; //Check the previous usage of a thousand separator. if (before.Contains(thousands)) { var split = before.Split(thousandsChar); //You can't add a thousand separators closer than 3 chars from each other. if (split.Last().Length != 3) return false; } } //After the carret. if (!string.IsNullOrEmpty(after)) { var split = after.Split(thousandsChar, decimalsChar); //You can't add a thousand separators closer than 3 chars from another separator, decimal or thousands. if (split.First().Length != 3) return true; } return false; } #endregion #region Decimal if (next.Equals(decimals)) { return !textBox.Text.Any(x => x.Equals(decimalsChar)); } #endregion return true; } private bool IsTextAllowed(string text) { return double.TryParse(text, out double result); //var regex = new Regex(@"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$", RegexOptions.CultureInvariant); //return regex.IsMatch(text); } #endregion } ================================================ FILE: ScreenToGif/Controls/DoubleUpDown.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; namespace ScreenToGif.Controls; /// /// Double only control with up and down buttons to change the value. /// public class DoubleUpDown : DoubleBox { #region Variables private RepeatButton _upButton; private RepeatButton _downButton; #endregion static DoubleUpDown() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DoubleUpDown), new FrameworkPropertyMetadata(typeof(DoubleUpDown))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); //Internal controls. _upButton = Template.FindName("UpButton", this) as RepeatButton; _downButton = Template.FindName("DownButton", this) as RepeatButton; if (_upButton != null) _upButton.Click += UpButton_Click; if (_downButton != null) _downButton.Click += DownButton_Click; } #region Event Handlers private void DownButton_Click(object sender, RoutedEventArgs e) { if (Value > Minimum) Value -= StepValue; } private void UpButton_Click(object sender, RoutedEventArgs e) { if (Value < Maximum) Value += StepValue; } #endregion } ================================================ FILE: ScreenToGif/Controls/DragScrollGrid.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; /// /// Scroll by Drag Grid /// TODO: Make a grid that reacts to the drag sideway to increase or decrease a number. /// public class DragScrollGrid : Grid { #region Variables private Point _lastPosition; public static readonly DependencyProperty IsDraggableProperty; #endregion #region Properties /// /// If true, will enable the value increase/decrease by sideway drag. /// public bool IsDraggable { get => (bool)GetValue(IsDraggableProperty); set => SetValue(IsDraggableProperty, value); } #endregion static DragScrollGrid() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DragScrollGrid), new FrameworkPropertyMetadata(typeof(DragScrollGrid))); IsDraggableProperty = DependencyProperty.Register("IsDraggable", typeof(bool), typeof(DragScrollGrid), new PropertyMetadata(false)); } public override void OnApplyTemplate() { base.OnApplyTemplate(); MouseDown += DragScrollGrid_MouseDown; MouseMove += DragScrollGrid_MouseMove; MouseUp += DragScrollGrid_MouseUp; } #region Events private void DragScrollGrid_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { Mouse.Capture(this); Cursor = Cursors.ScrollWE; } private void DragScrollGrid_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { if (!IsMouseCaptured) return; if ((int)_lastPosition.X == (int)e.GetPosition(this).X) return; if (_lastPosition.X > e.GetPosition(this).X) { //To the Left. //Value--; } //To the right. //Value++; } private void DragScrollGrid_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { ReleaseMouseCapture(); Cursor = Cursors.Arrow; } #endregion } ================================================ FILE: ScreenToGif/Controls/DrawingCanvas.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using ScreenToGif.Controls.Shapes; using ScreenToGif.Domain.Events; namespace ScreenToGif.Controls; internal class DrawingCanvas : Control { internal enum DrawingModes { None = 0, Ink, Select, EraseByPoint, EraseByObject, Shape } internal enum Shapes { Rectangle, Ellipse, Triangle, Arrow, Line, } #region Variables private Canvas _mainCanvas; private InkCanvas _mainInkCanvas; private AdornerLayer _adornerLayer; /// /// The start point for the drag operation. /// private Point _startPoint; /// /// The list of currently selected shapes. All selected shapes will have their own element adorner. /// private readonly List _selectedShapes = new List(); /// /// The current shape being drawn. /// private Shape _currentShape; /// /// The most distant point within the shape's boundary for the resize operation. /// private Point _mostDistantPoint; /// /// The less distant point (current point) within the shape's boundary for the resize operation. /// private Point _currentPoint; /// /// Horizontal orientation of the resize operation. /// private bool _isRightToLeft; /// /// Vertical orientation of the resize operation. /// private bool _isBottomToTop; #endregion #region Dependency properties internal static readonly DependencyProperty DrawingModeProperty = DependencyProperty.Register(nameof(DrawingMode), typeof(DrawingModes), typeof(DrawingCanvas), new PropertyMetadata(default(DrawingModes), DrawingMode_PropertyChanged)); internal static readonly DependencyProperty CurrentShapeProperty = DependencyProperty.Register(nameof(CurrentShape), typeof(Shapes), typeof(DrawingCanvas), new PropertyMetadata(default(Shapes))); internal static readonly DependencyProperty SelectionProperty = DependencyProperty.Register(nameof(Selection), typeof(Rect), typeof(DrawingCanvas), new PropertyMetadata(default(Rect))); internal static readonly DependencyProperty RenderRegionProperty = DependencyProperty.Register(nameof(RenderRegion), typeof(Rect), typeof(DrawingCanvas), new PropertyMetadata(default(Rect))); internal static readonly DependencyProperty IsDrawingProperty = DependencyProperty.Register(nameof(IsDrawing), typeof(bool), typeof(DrawingCanvas), new PropertyMetadata(false)); internal static readonly DependencyProperty ControlsZIndexProperty = DependencyProperty.Register(nameof(ControlsZIndex), typeof(long), typeof(DrawingCanvas), new PropertyMetadata(1L)); internal static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(nameof(StrokeThickness), typeof(double), typeof(DrawingCanvas), new PropertyMetadata(2d, Visual_PropertyChanged)); internal static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(nameof(Stroke), typeof(Brush), typeof(DrawingCanvas), new PropertyMetadata(Brushes.Black, Visual_PropertyChanged)); internal static readonly DependencyProperty FillProperty = DependencyProperty.Register(nameof(Fill), typeof(Brush), typeof(DrawingCanvas), new PropertyMetadata(Brushes.Transparent, Visual_PropertyChanged)); internal static readonly DependencyProperty RadiusProperty = DependencyProperty.Register(nameof(Radius), typeof(double), typeof(DrawingCanvas), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender, Visual_PropertyChanged)); public static readonly DependencyProperty StrokeDashArrayProperty = DependencyProperty.Register(nameof(StrokeDashArray), typeof(DoubleCollection), typeof(DrawingCanvas), new FrameworkPropertyMetadata(new DoubleCollection(), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, Visual_PropertyChanged)); #endregion #region Properties internal DrawingModes DrawingMode { get => (DrawingModes)GetValue(DrawingModeProperty); set => SetValue(DrawingModeProperty, value); } internal Shapes CurrentShape { get => (Shapes)GetValue(CurrentShapeProperty); set => SetValue(CurrentShapeProperty, value); } internal Rect Selection { get => (Rect)GetValue(SelectionProperty); set => SetValue(SelectionProperty, value); } internal Rect RenderRegion { get => (Rect)GetValue(RenderRegionProperty); set => SetValue(RenderRegionProperty, value); } internal bool IsDrawing { get => (bool)GetValue(IsDrawingProperty); set => SetValue(IsDrawingProperty, value); } internal long ControlsZIndex { get => (long)GetValue(ControlsZIndexProperty); set => SetValue(ControlsZIndexProperty, value); } [TypeConverter(typeof(LengthConverter))] public double StrokeThickness { get => (double)GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public Brush Stroke { get => (Brush)GetValue(StrokeProperty); set => SetValue(StrokeProperty, value); } public Brush Fill { get => (Brush)GetValue(FillProperty); set => SetValue(FillProperty, value); } [TypeConverter(typeof(LengthConverter))] public double Radius { get => (double)GetValue(RadiusProperty); set => SetValue(RadiusProperty, value); } public DoubleCollection StrokeDashArray { get => (DoubleCollection)GetValue(StrokeDashArrayProperty); set => SetValue(StrokeDashArrayProperty, value); } public int ShapesCount => _mainCanvas?.Children.Count ?? 0; #endregion static DrawingCanvas() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DrawingCanvas), new FrameworkPropertyMetadata(typeof(DrawingCanvas))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _mainCanvas = Template.FindName("MainCanvas", this) as Canvas; _mainInkCanvas = Template.FindName("MainInkCanvas", this) as InkCanvas; if (_mainInkCanvas != null) { _mainInkCanvas.PreviewMouseLeftButtonDown += MainInkCanvas_MouseLeftButtonDown; _mainInkCanvas.StrokeCollected += MainInkCanvas_StrokeCollected; } _adornerLayer = AdornerLayer.GetAdornerLayer(this); } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { Keyboard.Focus(this); _startPoint = e.GetPosition(this); switch (DrawingMode) { case DrawingModes.Select: { if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) { RemoveAllAdorners(); _selectedShapes.Clear(); } //When the user clicks exactly on top of a shape, it will be selected. var hitTest = _mainCanvas.Children.OfType().Where(w => w.Tag == null).FirstOrDefault(f => f.RenderedGeometry.FillContains(e.GetPosition(f))); if (hitTest != null) { SelectShape(hitTest); } else { //Starts drawing selection retangle. Selection = new Rect(_startPoint, new Size(0, 0)); CaptureMouse(); } break; } case DrawingModes.Shape: { RemoveAllAdorners(); RenderRegion = new Rect(_startPoint, new Size(0, 0)); IsDrawing = true; CaptureMouse(); CalculateOrientation(_startPoint, _startPoint); RenderShape(); break; } } e.Handled = true; base.OnMouseLeftButtonDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (!IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; if (DrawingMode == DrawingModes.Select && ((_selectedShapes?.Count ?? 0) == 0 || (Keyboard.Modifiers & ModifierKeys.Control) != 0)) { var current = GetBoundedCoordinates(e); Selection = new Rect(Math.Min(current.X, _startPoint.X), Math.Min(current.Y, _startPoint.Y), Math.Abs(current.X - _startPoint.X), Math.Abs(current.Y - _startPoint.Y)); } else if (DrawingMode == DrawingModes.Shape) { var current = GetBoundedCoordinates(e); RenderRegion = Rect.Inflate(new Rect(Math.Min(current.X, _startPoint.X), Math.Min(current.Y, _startPoint.Y), Math.Abs(current.X - _startPoint.X), Math.Abs(current.Y - _startPoint.Y)), -0.6d, -0.6d); CalculateOrientation(_startPoint, current); RenderShape(); } base.OnMouseMove(e); } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if (DrawingMode == DrawingModes.Select) { if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) { RemoveAllAdorners(); _selectedShapes.Clear(); } var selectedShapes = GetSelectedShapes(_mainCanvas, new RectangleGeometry(Selection)); //_mainCanvas.Children.OfType().Where(w => Selection.Contains(w.)).ToList(); if (selectedShapes.Any()) { foreach (var shape in selectedShapes) SelectShape(shape); } Selection = Rect.Empty; ReleaseMouseCapture(); } else if (DrawingMode == DrawingModes.Shape) { ReleaseMouseCapture(); RenderShape(); RemoveIfTooSmall(); IsDrawing = false; _selectedShapes?.Clear(); SelectShape(_currentShape); _currentShape = null; } base.OnMouseLeftButtonUp(e); } protected override void OnPreviewKeyDown(KeyEventArgs e) { switch (e.Key) { case Key.Back: case Key.Delete: RemoveAllAdorners(); RemoveAllSelectedShapes(); if (_selectedShapes.Count > 0) e.Handled = true; break; //TODO: Cntrl + C, Ctrl + V, } base.OnPreviewKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { double step; switch (Keyboard.Modifiers) { case ModifierKeys.Alt: step = 90; break; case ModifierKeys.Control: step = 1; break; case ModifierKeys.Shift: step = 20; break; default: return; } RotateAllSelectedShapes(e.Delta > 0 ? step : step * -1); base.OnMouseWheel(e); } #endregion #region Methods private Point GetBoundedCoordinates(MouseEventArgs e) { var current = e.GetPosition(this); if (current.X < -1) current.X = -1; if (current.Y < -1) current.Y = -1; if (current.X > ActualWidth) current.X = ActualWidth; if (current.Y > ActualHeight) current.Y = ActualHeight; return current; } private void RemoveAllAdorners() { if (_selectedShapes == null) return; foreach (var shape in _selectedShapes.Where(w => w != null)) { foreach (var adorner in _adornerLayer?.GetAdorners(shape)?.OfType() ?? new List()) _adornerLayer?.Remove(adorner); } } private void RemoveAllSelectedShapes() { if (_selectedShapes == null) return; foreach (var shape in _selectedShapes) _mainCanvas.Children.Remove(shape); _selectedShapes.Clear(); } private void RotateAllSelectedShapes(double angleDifference) { if (_selectedShapes == null) return; foreach (var shape in _selectedShapes) if (_adornerLayer.GetAdorners(shape)?[0] is ElementAdorner ad) ad.Angle += angleDifference; } private void CalculateOrientation(Point start, Point current) { _isBottomToTop = start.Y < current.Y; _isRightToLeft = start.X < current.X; _mostDistantPoint = start; _currentPoint = current; } private void RenderShape() { if (RenderRegion.IsEmpty) { if (_currentShape != null) _mainCanvas.Children.Remove(_currentShape); return; } if (_currentShape != null) { Canvas.SetTop(_currentShape, RenderRegion.Top); Canvas.SetLeft(_currentShape, RenderRegion.Left); _currentShape.Width = RenderRegion.Width; _currentShape.Height = RenderRegion.Height; if (_currentShape is Arrow arrow) { arrow.X1 = RenderRegion.Left - _mostDistantPoint.X; arrow.X2 = RenderRegion.Left - Math.Abs(_isRightToLeft ? _mostDistantPoint.X - _currentPoint.X : _currentPoint.X - _mostDistantPoint.X); arrow.Y1 = RenderRegion.Top - _mostDistantPoint.Y; arrow.Y2 = RenderRegion.Top - Math.Abs(_mostDistantPoint.Y - _currentPoint.Y); } return; } switch (CurrentShape) { case Shapes.Rectangle: _currentShape = new Rectangle { Width = RenderRegion.Width, Height = RenderRegion.Height, Stroke = Stroke, StrokeThickness = StrokeThickness, StrokeDashArray = StrokeDashArray, Fill = Fill, RadiusX = Radius, RadiusY = Radius }; break; case Shapes.Ellipse: _currentShape = new Ellipse { Width = RenderRegion.Width, Height = RenderRegion.Height, Stroke = Stroke, StrokeThickness = StrokeThickness, StrokeDashArray = StrokeDashArray, Fill = Fill, }; break; case Shapes.Triangle: _currentShape = new Triangle { Width = RenderRegion.Width, Height = RenderRegion.Height, Stroke = Stroke, StrokeThickness = StrokeThickness, StrokeDashArray = StrokeDashArray, Fill = Fill, //RadiusX = Radius, //RadiusY = Radius }; break; case Shapes.Arrow: _currentShape = new Arrow { Width = RenderRegion.Width, Height = RenderRegion.Height, Stroke = Stroke, StrokeThickness = StrokeThickness, StrokeDashArray = StrokeDashArray, Fill = Fill, Stretch = Stretch.Fill, HeadHeight = 10, HeadWidth = 10, X1 = RenderRegion.Left - _mostDistantPoint.X, X2 = RenderRegion.Left - Math.Abs(_isRightToLeft ? _mostDistantPoint.X - _currentPoint.X : _currentPoint.X - _mostDistantPoint.X), Y1 = RenderRegion.Top - _mostDistantPoint.Y, Y2 = RenderRegion.Top - Math.Abs(_mostDistantPoint.Y - _currentPoint.Y) }; break; } if (_currentShape == null) return; _mainCanvas.Children.Add(_currentShape); Canvas.SetLeft(_currentShape, RenderRegion.Left); Canvas.SetTop(_currentShape, RenderRegion.Top); Panel.SetZIndex(_currentShape, _mainCanvas.Children.OfType().Where(w => w.Tag == null).Max(Panel.GetZIndex) + 1); } private void RemoveIfTooSmall() { if (!(RenderRegion.Width + RenderRegion.Height < 10)) return; _mainCanvas.Children.Remove(_currentShape); } private List GetSelectedShapes(Visual element, Geometry geometry) { var shapes = new List(); VisualTreeHelper.HitTest(element, null, result => { if (result.VisualHit is Shape shape && shape.Tag == null) shapes.Add(shape); return HitTestResultBehavior.Continue; }, new GeometryHitTestParameters(geometry)); return shapes; } private void SelectShape(Shape shape) { if (shape == null) return; if (!_selectedShapes.Contains(shape)) _selectedShapes.Add(shape); AdjustDepth(); var adorner = new ElementAdorner(shape, true, true, true, _mainCanvas, _startPoint); adorner.Manipulated += Adorner_Manipulated; adorner.RotationResetRequested += Adorner_RotationResetRequested; adorner.Removed += Adorner_Removed; adorner.MouseLeftButtonDown += Adorner_MouseLeftButtonDown; _adornerLayer.Add(adorner); } private void DeselectShape(Shape shape) { if (shape == null) return; if (!_selectedShapes.Contains(shape)) return; _selectedShapes.Remove(shape); foreach (var adorner in _adornerLayer?.GetAdorners(shape)?.OfType() ?? new List()) _adornerLayer?.Remove(adorner); } private void AdjustDepth() { //0 = Further behind. //999 = Further in front. var indexes = _mainCanvas.Children.OfType().Where(w => w.Tag == null).Select(Panel.GetZIndex).OrderBy(o => o).ToList(); if (indexes.Count == 0) return; //Make all shapes go 1 step behind. foreach (var shape in _mainCanvas.Children.OfType().Where(w => w.Tag == null)) Panel.SetZIndex(shape, indexes.IndexOf(Panel.GetZIndex(shape))); //In order to show the selected shapes in front, the Z order should be greater than the rest of the shapes. var max = _mainCanvas.Children.OfType().Where(w => w.Tag == null).Max(Panel.GetZIndex); //Make all selected shapes go 1 step to the front, making sure to respect the current Z order. foreach (var shape in _selectedShapes.OrderBy(Panel.GetZIndex)) Panel.SetZIndex(shape, ++max); //All design controls should be at the top. ControlsZIndex = ++max; } public void DeselectAll() { RemoveAllAdorners(); _selectedShapes?.Clear(); } public void RemoveAllShapes() { DeselectAll(); _mainCanvas.Children.Clear(); } #endregion #region Events private static void DrawingMode_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not DrawingCanvas canvas || canvas._mainInkCanvas == null) return; canvas._mainInkCanvas.Visibility = canvas.DrawingMode == DrawingModes.Ink ? Visibility.Visible : Visibility.Collapsed; } private static void Visual_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not DrawingCanvas canvas) return; if (canvas._mainInkCanvas != null) { canvas._mainInkCanvas.DefaultDrawingAttributes = new DrawingAttributes { Color = canvas.Stroke is SolidColorBrush color ? color.Color : Colors.Black, Height = Math.Max(canvas.StrokeThickness, 1), Width = Math.Max(canvas.StrokeThickness, 1), }; } if (canvas._selectedShapes == null) return; //Change the settings of the selected shapes. foreach (var shape in canvas._selectedShapes) { shape.Stroke = canvas.Stroke; shape.StrokeThickness = canvas.StrokeThickness; shape.StrokeDashArray = canvas.StrokeDashArray; shape.Fill = canvas.Fill; if (shape is Rectangle rect) rect.RadiusX = rect.RadiusY = canvas.Radius; } } private void Adorner_Manipulated(object sender, ManipulatedEventArgs args) { if (sender is not ElementAdorner adorner) return; foreach (var shape in _selectedShapes) { if (Equals(shape, adorner.AdornedElement)) continue; if (_adornerLayer.GetAdorners(shape)?[0] is ElementAdorner ad) ad.Angle += args.AngleDifference; if (Math.Abs(args.HeightDifference) > 0.1 && shape.ActualHeight + args.HeightDifference > 10 && shape.ActualHeight + args.HeightDifference <= _mainCanvas.ActualHeight) shape.Height += args.HeightDifference; //shape.Height = shape.ActualHeight + args.HeightDifference; if (Math.Abs(args.WidthDifference) > 0.1 && shape.ActualWidth + args.WidthDifference > 10 && shape.ActualWidth + args.WidthDifference <= _mainCanvas.ActualWidth) shape.Width += args.WidthDifference; //shape.Width = shape.ActualWidth + args.WidthDifference; if (Canvas.GetTop(shape) + args.TopDifference >= 0 && Canvas.GetTop(shape) + args.TopDifference + shape.ActualHeight < _mainCanvas.ActualHeight) Canvas.SetTop(shape, Canvas.GetTop(shape) + args.TopDifference); if (Canvas.GetLeft(shape) + args.LeftDifference >= 0 && Canvas.GetLeft(shape) + args.LeftDifference + shape.ActualWidth < _mainCanvas.ActualWidth) Canvas.SetLeft(shape, Canvas.GetLeft(shape) + args.LeftDifference); } } private void Adorner_RotationResetRequested(object sender, RoutedEventArgs e) { if (sender is not ElementAdorner adorner) return; foreach (var shape in _selectedShapes) { if (_adornerLayer.GetAdorners(shape)?[0] is ElementAdorner ad) ad.Angle = 0; } } private void Adorner_Removed(object sender, RoutedEventArgs e) { RemoveAllAdorners(); RemoveAllSelectedShapes(); } private void Adorner_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) return; var adorner = sender as ElementAdorner; var shape = adorner?.AdornedElement as Shape; if (_selectedShapes.Contains(shape)) DeselectShape(shape); } private void MainInkCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { RemoveAllAdorners(); _selectedShapes.Clear(); } private void MainInkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e) { foreach (var stroke in _mainInkCanvas.Strokes) { if (stroke.GetBounds().Width < 12 || stroke.GetBounds().Height < 12) continue; var shape = new Polyline { Stroke = new SolidColorBrush(stroke.DrawingAttributes.Color), StrokeThickness = StrokeThickness, //How? These strokes can receive pressure info. StrokeDashArray = StrokeDashArray, FillRule = FillRule.EvenOdd, Stretch = Stretch.Fill }; var points = new PointCollection(); var minTop = stroke.StylusPoints.Min(m => m.Y); var minLeft = stroke.StylusPoints.Min(m => m.X); foreach (var point in stroke.StylusPoints) { var x = point.X - minLeft; var y = point.Y - minTop; points.Add(new Point(x, y)); } shape.Points = points; _mainCanvas.Children.Add(shape); SelectShape(shape); Canvas.SetLeft(shape, minLeft); Canvas.SetTop(shape, minTop); AdjustDepth(); } _mainInkCanvas.Strokes.Clear(); Keyboard.Focus(this); } #endregion } ================================================ FILE: ScreenToGif/Controls/DropDownButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// A non-editable ComboBox style. /// public class DropDownButton : ComboBox { #region Variables public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(DropDownButton)); public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(DropDownButton), new FrameworkPropertyMetadata()); public static readonly DependencyProperty MaxSizeProperty = DependencyProperty.Register(nameof(MaxSize), typeof(double), typeof(DropDownButton), new FrameworkPropertyMetadata(26.0)); public static readonly DependencyProperty IsVerticalProperty = DependencyProperty.Register(nameof(IsVertical), typeof(bool), typeof(DropDownButton), new FrameworkPropertyMetadata(false)); #endregion #region Properties /// /// The icon of the button. /// [Description("The icon of the DropDownButton."), Category("Common")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image."), Category("Common")] public double MaxSize { get => (double)GetValue(MaxSizeProperty); set => SetCurrentValue(MaxSizeProperty, value); } /// /// The text of the control. /// [Description("The text of the control."), Category("Common")] public string Description { get => (string)GetValue(DescriptionProperty); set => SetCurrentValue(DescriptionProperty, value); } /// /// True if vertical style. /// [Description("True if vertical style."), Category("Common")] public bool IsVertical { get => (bool)GetValue(IsVerticalProperty); set => SetCurrentValue(IsVerticalProperty, value); } #endregion static DropDownButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DropDownButton), new FrameworkPropertyMetadata(typeof(DropDownButton))); } } ================================================ FILE: ScreenToGif/Controls/DynamicGrid.cs ================================================ using System; using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; public class DynamicGrid : Grid { #region Dependency Properties public static readonly DependencyProperty FirstColumnProperty = DependencyProperty.Register(nameof(FirstColumn), typeof(int), typeof(DynamicGrid), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), ValidateFirstColumn); public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register(nameof(Columns), typeof(int), typeof(DynamicGrid), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), ValidateColumns); public static readonly DependencyProperty RowsProperty = DependencyProperty.Register(nameof(Rows), typeof(int), typeof(DynamicGrid), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), ValidateRows); public static readonly DependencyProperty IsReversedProperty = DependencyProperty.Register(nameof(IsReversed), typeof(bool), typeof(DynamicGrid), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); #endregion #region Properties ///Gets or sets the number of leading blank cells in the first row of the grid. ///The number of empty cells that are in the first row of the grid. The default is 0. public int FirstColumn { get => (int)GetValue(FirstColumnProperty); set => SetValue(FirstColumnProperty, value); } /// Gets or sets the number of columns that are in the grid. /// The number of columns that are in the grid. The default is 0. public int Columns { get => (int)GetValue(ColumnsProperty); set => SetValue(ColumnsProperty, value); } /// Gets or sets the number of rows that are in the grid. /// The number of rows that are in the grid. The default is 0. public int Rows { get => (int)GetValue(RowsProperty); set => SetValue(RowsProperty, value); } public bool IsReversed { get => (bool)GetValue(IsReversedProperty); set => SetValue(IsReversedProperty, value); } #endregion #region Coerce private static bool ValidateFirstColumn(object o) { return (int)o >= 0; } private static bool ValidateRows(object o) { return (int)o >= 0; } private static bool ValidateColumns(object o) { return (int)o >= 0; } #endregion protected override Size MeasureOverride(Size constraint) { UpdateComputedValues(); RowDefinitions.Clear(); ColumnDefinitions.Clear(); for (var i = 0; i < Rows; i++) RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); for (var i = 0; i < Columns; i++) ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var count = 0; for (var r = 0; r < Rows; r++) for (var c = 0; c < Columns; c++) { if (count > Children.Count - 1) continue; Children[count].Measure(constraint); if (RowDefinitions[r].MinHeight < Children[count].DesiredSize.Height) RowDefinitions[r].MinHeight = Children[count].DesiredSize.Height; if (ColumnDefinitions[c].MinWidth < Children[count].DesiredSize.Width) ColumnDefinitions[c].MinWidth = Children[count].DesiredSize.Width + 6; SetColumn(Children[count], c); SetRow(Children[count], r); count++; } return base.MeasureOverride(constraint); } //protected Size ArrangeOverride2(Size arrangeSize) //{ // var finalRect = new Rect(0.0, 0.0, arrangeSize.Width / Columns, arrangeSize.Height / Rows); // var width = finalRect.Width; // var num = arrangeSize.Width - 1.0; // finalRect.X += finalRect.Width * FirstColumn; // if (IsReversed) // { // for (var i = InternalChildren.Count - 1; i >= 0; i--) // { // InternalChildren[i].Arrange(finalRect); // if (InternalChildren[i].Visibility != Visibility.Collapsed) // { // finalRect.X += width; // if (finalRect.X >= num) // { // finalRect.Y += finalRect.Height; // finalRect.X = 0.0; // } // } // } // return arrangeSize; // } // foreach (UIElement internalChild in InternalChildren) // { // internalChild.Arrange(finalRect); // if (internalChild.Visibility != Visibility.Collapsed) // { // finalRect.X += width; // if (finalRect.X >= num) // { // finalRect.Y += finalRect.Height; // finalRect.X = 0.0; // } // } // } // return arrangeSize; //} private void UpdateComputedValues() { if (FirstColumn >= Columns) FirstColumn = 0; if (Rows != 0 && Columns != 0) return; var num = 0; var index = 0; for (var count = InternalChildren.Count; index < count; ++index) { if (InternalChildren[index].Visibility != Visibility.Collapsed) ++num; } if (num == 0) num = 1; if (Rows == 0) { if (Columns > 0) { Rows = (num + FirstColumn + (Columns - 1)) / Columns; } else { Rows = (int)Math.Sqrt(num); if (Rows * Rows < num) Rows = Rows + 1; Columns = Rows; } } else { if (Columns != 0) return; Columns = (num + (Rows - 1)) / Rows; } } } ================================================ FILE: ScreenToGif/Controls/ElementAdorner.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using ScreenToGif.Domain.Events; namespace ScreenToGif.Controls; internal class ElementAdorner : Adorner { #region Dependency properties public static readonly DependencyProperty AngleProperty = DependencyProperty.Register("Angle", typeof(double), typeof(ElementAdorner), new PropertyMetadata(0d, Angle_PropertyChanged)); private static void Angle_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var adorner = d as ElementAdorner; if (adorner == null || !adorner.CanRotate) return; //adorner._adornedElement.RenderTransformOrigin = new Point(0.5, 0.5); adorner._adornedElement.RenderTransform = new RotateTransform(adorner.Angle); //adorner._rotationThumb.Angle = adorner.Angle; } public double Angle { get => (double)GetValue(AngleProperty); set => SetValue(AngleProperty, value); } #endregion #region Variables and Properties private readonly VisualCollection _visualChildren; private readonly FrameworkElement _adornedElement; private readonly FrameworkElement _parent; /// /// The rectangle that surrounds the adorned element. /// private Rectangle _borderRectangle; /// /// Resizing adorner uses Thumbs for visual elements. /// The Thumbs have built-in mouse input handling. /// private readonly Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _middleBottom, _middleTop, _leftMiddle, _rightMiddle; /// /// The thumb that allows the rotation of the adorned element. /// private Thumb _rotationThumb; private Vector _startVector; private Point _centerPoint; private double _startAngle; /// /// The start point for the drag operation. /// internal Point StartPoint { get; set; } internal bool CanMove { get; set; } internal bool CanResize { get; set; } internal bool CanRotate { get; set; } #endregion public ElementAdorner(FrameworkElement adornedElement, bool canMove, bool canResize, bool canRotate, FrameworkElement parent, Point startPoint) : base(adornedElement) { #region Properties _visualChildren = new VisualCollection(this); _adornedElement = adornedElement; _parent = parent ?? _adornedElement?.Parent as FrameworkElement; StartPoint = startPoint; CanMove = canMove; CanResize = canResize; CanRotate = canRotate; #endregion #region Refresh size if (double.IsNaN(_adornedElement.Width)) { _adornedElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); _adornedElement.Arrange(new Rect(Canvas.GetLeft(_adornedElement), Canvas.GetTop(_adornedElement), _adornedElement.DesiredSize.Width, _adornedElement.DesiredSize.Height)); } #endregion #region Build inner elements //Creates the dashed rectangle around the adorned element. BuildAdornerBorder(); if (CanMove) { //Allows the drag operation to move the adorned object. _borderRectangle.PreviewMouseLeftButtonDown += AdornedElement_PreviewMouseLeftButtonDown; _borderRectangle.MouseMove += AdornedElement_MouseMove; _borderRectangle.MouseUp += AdornedElement_MouseUp; } if (CanResize) { //Call a helper method to initialize the Thumbs with a customized cursors. BuildAdornerThumb(ref _topLeft, Cursors.SizeNWSE); BuildAdornerThumb(ref _topRight, Cursors.SizeNESW); BuildAdornerThumb(ref _bottomLeft, Cursors.SizeNESW); BuildAdornerThumb(ref _bottomRight, Cursors.SizeNWSE); BuildAdornerThumb(ref _middleBottom, Cursors.SizeNS); BuildAdornerThumb(ref _middleTop, Cursors.SizeNS); BuildAdornerThumb(ref _leftMiddle, Cursors.SizeWE); BuildAdornerThumb(ref _rightMiddle, Cursors.SizeWE); //Add handlers for resizing • Corners _bottomLeft.DragDelta += HandleBottomLeft; _bottomRight.DragDelta += HandleBottomRight; _topLeft.DragDelta += HandleTopLeft; _topRight.DragDelta += HandleTopRight; //Add handlers for resizing • Sides _middleBottom.DragDelta += HandleBottom; _middleTop.DragDelta += HandleTop; _leftMiddle.DragDelta += HandleLeft; _rightMiddle.DragDelta += HandleRight; } if (CanRotate) { //Creates the thumb that allows the rotation of the adorned element. BuildAdornerRotator(); } #endregion } #region Overrides /// /// Arrange the Adorners. /// /// The final Size /// The final size protected override Size ArrangeOverride(Size finalSize) { // desiredWidth and desiredHeight are the width and height of the element that's being adorned. // These will be used to place the ResizingAdorner at the corners of the adorned element. var desiredWidth = AdornedElement.DesiredSize.Width; var desiredHeight = AdornedElement.DesiredSize.Height; // adornerWidth & adornerHeight are used for placement as well. var adornerWidth = this.DesiredSize.Width; var adornerHeight = this.DesiredSize.Height; _topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight)); _topRight.Arrange(new Rect(adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight)); _bottomLeft.Arrange(new Rect(-adornerWidth / 2, adornerHeight / 2, adornerWidth, adornerHeight)); _bottomRight.Arrange(new Rect(adornerWidth / 2, adornerHeight / 2, adornerWidth, adornerHeight)); _middleBottom.Arrange(new Rect(0, adornerHeight / 2, adornerWidth, adornerHeight)); _middleTop.Arrange(new Rect(0, -adornerHeight / 2, adornerWidth, adornerHeight)); _leftMiddle.Arrange(new Rect(-adornerWidth / 2, 0, adornerWidth, adornerHeight)); _rightMiddle.Arrange(new Rect(adornerWidth / 2, 0, adornerWidth, adornerHeight)); //var zoomFactor = GetCanvasZoom(AdornedElement); //_borderRectangle.Arrange(new Rect(0, 0, adornerWidth * zoomFactor, adornerHeight * zoomFactor)); _borderRectangle.Arrange(new Rect(0, 0, adornerWidth, adornerHeight)); _rotationThumb.Arrange(new Rect(0, (-adornerHeight / 2) - 15, adornerWidth, adornerHeight)); return finalSize; } /// /// Override the VisualChildrenCount and GetVisualChild properties to interface with the adorner's visual collection. /// protected override int VisualChildrenCount => _visualChildren.Count; /// /// Gets the VisualChildren at given position. /// /// The Index to look for. /// The VisualChildren protected override Visual GetVisualChild(int index) { return _visualChildren[index]; } protected override void OnPreviewKeyDown(KeyEventArgs e) { //Propagate the event to the parent control. var args = new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key) { RoutedEvent = e.RoutedEvent, Source = e.Source, }; _parent.RaiseEvent(args); base.OnPreviewKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { //Propagate the event to the parent control. var args = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = e.RoutedEvent, Source = this, }; _parent.RaiseEvent(args); base.OnMouseWheel(e); } #endregion #region Events public static readonly RoutedEvent ManipulatedEvent = EventManager.RegisterRoutedEvent("Manipulated", RoutingStrategy.Bubble, typeof(ManipulatedEventHandler), typeof(ElementAdorner)); public static readonly RoutedEvent RotationResetRequestedEvent = EventManager.RegisterRoutedEvent("RotationResetRequested", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ElementAdorner)); public static readonly RoutedEvent DuplicatedEvent = EventManager.RegisterRoutedEvent("Duplicated", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ElementAdorner)); public static readonly RoutedEvent RemovedEvent = EventManager.RegisterRoutedEvent("Removed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ElementAdorner)); public event ManipulatedEventHandler Manipulated { add => AddHandler(ManipulatedEvent, value); remove => RemoveHandler(ManipulatedEvent, value); } public event RoutedEventHandler RotationResetRequested { add => AddHandler(RotationResetRequestedEvent, value); remove => RemoveHandler(RotationResetRequestedEvent, value); } public event RoutedEventHandler Duplicated { add => AddHandler(DuplicatedEvent, value); remove => RemoveHandler(DuplicatedEvent, value); } public event RoutedEventHandler Removed { add => AddHandler(RemovedEvent, value); remove => RemoveHandler(RemovedEvent, value); } void RaiseManipulatedEvent(double angleDiff) { if (ManipulatedEvent == null) return; RaiseEvent(new ManipulatedEventArgs(ManipulatedEvent, angleDiff, 0, 0, 0, 0)); } void RaiseManipulatedEvent(double widthDiff, double heightDiff, double topDiff = 0, double leftDiff = 0) { if (ManipulatedEvent == null || (Math.Abs(widthDiff) < 0.001 && Math.Abs(heightDiff) < 0.001 && Math.Abs(leftDiff) < 0.001 && Math.Abs(topDiff) < 0.001)) return; RaiseEvent(new ManipulatedEventArgs(ManipulatedEvent, widthDiff, heightDiff, topDiff, leftDiff)); } void RaiseRotationResetRequestedEvent() { if (RotationResetRequestedEvent == null) return; RaiseEvent(new RoutedEventArgs(RotationResetRequestedEvent)); } void RaiseDuplicatedEvent() { if (DuplicatedEvent == null) return; RaiseEvent(new RoutedEventArgs(DuplicatedEvent)); } void RaiseRemovedEvent() { if (RemovedEvent == null) return; RaiseEvent(new RoutedEventArgs(RemovedEvent)); } #endregion #region Methods /// /// Creates the dashed border around the adorned element. /// private void BuildAdornerBorder() { var resetMenu = new ExtendedMenuItem { Header = "Reset rotation", Icon = TryFindResource("Vector.Repeat") as Brush }; resetMenu.SetResourceReference(HeaderedItemsControl.HeaderProperty, "S.Shapes.Shapes.ResetRotatio"); //var duplicateMenu = new ImageMenuItem { Header = "Duplicate", Image = TryFindResource("Vector.Copy") as Canvas }; var removeMenu = new ExtendedMenuItem { Header = "Remove", Icon = TryFindResource("Vector.Cancel") as Brush }; removeMenu.SetResourceReference(HeaderedItemsControl.HeaderProperty, "S.Shapes.Shapes.Remove"); resetMenu.Click += (sender, args) => RaiseRotationResetRequestedEvent(); //duplicateMenu.Click += (sender, args) => RaiseDuplicatedEvent(); removeMenu.Click += (sender, args) => RaiseRemovedEvent(); _borderRectangle = new Rectangle { Stroke = new SolidColorBrush(Color.FromRgb(171, 171, 171)), StrokeThickness = 1, Fill = Brushes.Transparent, StrokeDashArray = new DoubleCollection { 5 }, Cursor = Cursors.SizeAll, UseLayoutRounding = true, SnapsToDevicePixels = true, ContextMenu = new ContextMenu { Items = { resetMenu, removeMenu } } }; _visualChildren.Add(_borderRectangle); } /// /// Instantiates the corner Thumbs, setting the Cursor property, some appearance properties, and add the elements to the visual tree. /// /// The Thumb to Instantiate. /// The custom cursor. private void BuildAdornerThumb(ref Thumb thumb, Cursor cursor) { if (thumb != null) return; thumb = new Thumb { Cursor = cursor, Height = 10, Width = 10, Style = (Style)FindResource("ScrollBar.Thumb"), }; _visualChildren.Add(thumb); } /// /// Creates the element that allows the adorned element to be rotated. /// private void BuildAdornerRotator() { _rotationThumb = new Thumb { Height = 10, Width = 10, Cursor = Cursors.SizeAll, Style = FindResource("Style.Thumb.Ellipse") as Style }; _rotationThumb.DragStarted += RotationThumb_DragStarted; _rotationThumb.DragDelta += RotationThumb_DragDelta; _adornedElement.RenderTransformOrigin = new Point(0.5, 0.5); if (_adornedElement.RenderTransform is RotateTransform transform) Angle = transform.Angle; _visualChildren.Add(_rotationThumb); } private void AfterManipulation() { InvalidateVisual(); UpdateLayout(); } #endregion #region Events private void AdornedElement_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (_parent == null) return; if (_borderRectangle.CaptureMouse()) StartPoint = e.GetPosition(_parent); } private void AdornedElement_MouseMove(object sender, MouseEventArgs e) { if (_parent == null || e.LeftButton != MouseButtonState.Pressed) return; _borderRectangle.MouseMove -= AdornedElement_MouseMove; var currentPosition = e.GetPosition(_parent); var x = Canvas.GetLeft(_adornedElement) + (currentPosition.X - StartPoint.X); var y = Canvas.GetTop(_adornedElement) + (currentPosition.Y - StartPoint.Y); if (x < -1) x = -1; if (y < -1) y = -1; if (x + _adornedElement.DesiredSize.Width > _parent.ActualWidth + 1) x = _parent.ActualWidth + 1 - _adornedElement.DesiredSize.Width; if (y + _adornedElement.DesiredSize.Height > _parent.ActualHeight + 1) y = _parent.ActualHeight + 1 - _adornedElement.DesiredSize.Height; RaiseManipulatedEvent(0, 0, y - Canvas.GetTop(_adornedElement), x - Canvas.GetLeft(_adornedElement)); Canvas.SetLeft(_adornedElement, x); Canvas.SetTop(_adornedElement, y); StartPoint = currentPosition; e.Handled = true; _borderRectangle.MouseMove += AdornedElement_MouseMove; AfterManipulation(); } private void AdornedElement_MouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs) { _borderRectangle?.ReleaseMouseCapture(); } /// ///Handler for resizing from the top-left. /// private void HandleTopLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width - e.HorizontalChange, 10); var left = Canvas.GetLeft(_adornedElement) - (width - _adornedElement.DesiredSize.Width); var height = Math.Max(_adornedElement.DesiredSize.Height - e.VerticalChange, 10); var top = Canvas.GetTop(_adornedElement) - (height - _adornedElement.DesiredSize.Height); if (top < 0) { height -= top * -1; top = 0; } if (left < 0) { width -= left * -1; left = 0; } RaiseManipulatedEvent(width - _adornedElement.Width, height - _adornedElement.Height, top - Canvas.GetTop(_adornedElement), left - Canvas.GetLeft(_adornedElement)); Canvas.SetLeft(_adornedElement, left); Canvas.SetTop(_adornedElement, top); _adornedElement.Height = height; _adornedElement.Width = width; //TODO: Maybe trap mouse while dragging with ClipCursor(ref r); AfterManipulation(); } /// /// Handler for resizing from the top-right. /// private void HandleTopRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width + e.HorizontalChange, 10); var height = Math.Max(_adornedElement.DesiredSize.Height - e.VerticalChange, 10); var top = Canvas.GetTop(_adornedElement) - (height - _adornedElement.DesiredSize.Height); var left = Canvas.GetLeft(_adornedElement); if (top < 0) { height -= top * -1; top = 0; } if (left + width > _parent.ActualWidth) width = _parent.ActualWidth - left; RaiseManipulatedEvent(width - _adornedElement.Width, height - _adornedElement.Height, top - Canvas.GetTop(_adornedElement)); Canvas.SetTop(_adornedElement, top); _adornedElement.Height = height; _adornedElement.Width = width; AfterManipulation(); } /// /// Handler for resizing from the bottom-left. /// private void HandleBottomLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width - e.HorizontalChange, 10); var left = Canvas.GetLeft(_adornedElement) - (width - _adornedElement.DesiredSize.Width); var height = Math.Max(_adornedElement.DesiredSize.Height + e.VerticalChange, 10); if (left < 0) { width -= left * -1; left = 0; } if (Canvas.GetLeft(_adornedElement) + width > _parent.ActualWidth) width = _parent.ActualWidth - Canvas.GetLeft(_adornedElement); if (Canvas.GetTop(_adornedElement) + height > _parent.ActualHeight) height = _parent.ActualHeight - Canvas.GetTop(_adornedElement); RaiseManipulatedEvent(width - _adornedElement.Width, height - _adornedElement.Height, 0, left - Canvas.GetLeft(_adornedElement)); Canvas.SetLeft(_adornedElement, left); _adornedElement.Height = height; _adornedElement.Width = width; AfterManipulation(); } /// /// Handler for resizing from the bottom-right. /// private void HandleBottomRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width + e.HorizontalChange, 10); var height = Math.Max(_adornedElement.DesiredSize.Height + e.VerticalChange, 10); var top = Canvas.GetTop(_adornedElement); var left = Canvas.GetLeft(_adornedElement); if (left + width > _parent.ActualWidth) width = _parent.ActualWidth - left; if (top + height > _parent.ActualHeight) height = _parent.ActualHeight - top; RaiseManipulatedEvent(width - _adornedElement.Width, height - _adornedElement.Height); _adornedElement.Height = height; _adornedElement.Width = width; AfterManipulation(); } /// /// Handler for resizing from the left-middle. /// private void HandleLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width - e.HorizontalChange, 10); var left = Canvas.GetLeft(_adornedElement) - (width - _adornedElement.DesiredSize.Width); if (left < 0) { width -= left * -1; left = 0; } RaiseManipulatedEvent(width - _adornedElement.Width, 0, 0, left - Canvas.GetLeft(_adornedElement)); Canvas.SetLeft(_adornedElement, left); _adornedElement.Width = width; AfterManipulation(); } /// /// Handler for resizing from the top-middle. /// private void HandleTop(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(_adornedElement.DesiredSize.Height - e.VerticalChange, 10); var top = Canvas.GetTop(_adornedElement) - (height - _adornedElement.DesiredSize.Height); if (top < 0) { height -= top * -1; top = 0; } RaiseManipulatedEvent(0, height - _adornedElement.Height, top - Canvas.GetTop(_adornedElement)); Canvas.SetTop(_adornedElement, top); _adornedElement.Height = height; AfterManipulation(); } /// /// Handler for resizing from the right-middle. /// private void HandleRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(_adornedElement.DesiredSize.Width + e.HorizontalChange, 10); if (Canvas.GetLeft(_adornedElement) + width > _parent.ActualWidth) width = _parent.ActualWidth - Canvas.GetLeft(_adornedElement); RaiseManipulatedEvent(width - _adornedElement.Width, 0); _adornedElement.Width = width; AfterManipulation(); } /// /// Handler for resizing from the bottom-middle. /// private void HandleBottom(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(_adornedElement.DesiredSize.Height + e.VerticalChange, 10); if (Canvas.GetTop(_adornedElement) + height > _parent.ActualHeight) height = _parent.ActualHeight - Canvas.GetTop(_adornedElement); RaiseManipulatedEvent(0, height - _adornedElement.Height); _adornedElement.Height = height; AfterManipulation(); } /// /// Handler for the start of the drag operation of the thumb that allows the rotation of the shape. /// private void RotationThumb_DragStarted(object sender, DragStartedEventArgs e) { if (_adornedElement == null || _parent == null) return; _centerPoint = _adornedElement.TranslatePoint(new Point(_adornedElement.ActualWidth * _adornedElement.RenderTransformOrigin.X, _adornedElement.ActualHeight * _adornedElement.RenderTransformOrigin.Y), _parent); _startVector = Point.Subtract(Mouse.GetPosition(_parent), _centerPoint); _startAngle = Angle; } /// /// Handler for the drag operation of the thumb that allows the rotation of the shape. /// private void RotationThumb_DragDelta(object sender, DragDeltaEventArgs e) { if (_adornedElement == null || _parent == null) return; var deltaVector = Point.Subtract(Mouse.GetPosition(_parent), _centerPoint); var angle = Vector.AngleBetween(_startVector, deltaVector); var newAngle = _startAngle + Math.Round(angle, 0); RaiseManipulatedEvent(newAngle - Angle); Angle = newAngle; _adornedElement.InvalidateMeasure(); } #endregion } ================================================ FILE: ScreenToGif/Controls/EncoderListViewItem.cs ================================================ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using ScreenToGif.Domain.Enums; using ScreenToGif.ImageUtil; using ScreenToGif.Util; using ScreenToGif.Util.Extensions; using ScreenToGif.Windows.Other; using Clipboard = System.Windows.Clipboard; namespace ScreenToGif.Controls; /// /// ListViewItem used by the Encoder window. /// public class EncoderListViewItem : ListViewItem { #region Dependency Properties public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(EncoderListViewItem)); public static readonly DependencyProperty PercentageProperty = DependencyProperty.Register(nameof(Percentage), typeof(double), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty CurrentFrameProperty = DependencyProperty.Register(nameof(CurrentFrame), typeof(int), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(1)); public static readonly DependencyProperty FrameCountProperty = DependencyProperty.Register(nameof(FrameCount), typeof(int), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(0)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty IdProperty = DependencyProperty.Register(nameof(Id), typeof(int), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(-1)); public static readonly DependencyProperty TokenSourceProperty = DependencyProperty.Register(nameof(TokenSource), typeof(CancellationTokenSource), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty IsIndeterminateProperty = DependencyProperty.Register(nameof(IsIndeterminate), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(nameof(Status), typeof(EncodingStatus), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(EncodingStatus.Processing)); public static readonly DependencyProperty OutputTypeProperty = DependencyProperty.Register(nameof(OutputType), typeof(ExportFormats), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(ExportFormats.Gif)); public static readonly DependencyProperty SizeInBytesProperty = DependencyProperty.Register(nameof(SizeInBytes), typeof(long), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(0L)); public static readonly DependencyProperty OutputPathProperty = DependencyProperty.Register(nameof(OutputPath), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty OutputFilenameProperty = DependencyProperty.Register(nameof(OutputFilename), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(OutputFilename_PropertyChanged)); public static readonly DependencyProperty SavedToDiskProperty = DependencyProperty.Register(nameof(SavedToDisk), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty AreMultipleFilesProperty = DependencyProperty.Register(nameof(AreMultipleFiles), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty ExceptionProperty = DependencyProperty.Register(nameof(Exception), typeof(Exception), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty UploadedProperty = DependencyProperty.Register(nameof(Uploaded), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty UploadLinkProperty = DependencyProperty.Register(nameof(UploadLink), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty UploadLinkDisplayProperty = DependencyProperty.Register(nameof(UploadLinkDisplay), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty DeletionLinkProperty = DependencyProperty.Register(nameof(DeletionLink), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty UploadTaskExceptionProperty = DependencyProperty.Register(nameof(UploadTaskException), typeof(Exception), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty CopiedToClipboardProperty = DependencyProperty.Register(nameof(CopiedToClipboard), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty CopyTaskExceptionProperty = DependencyProperty.Register(nameof(CopyTaskException), typeof(Exception), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty CommandExecutedProperty = DependencyProperty.Register(nameof(CommandExecuted), typeof(bool), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty CommandTaskExceptionProperty = DependencyProperty.Register(nameof(CommandTaskException), typeof(Exception), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty CommandOutputProperty = DependencyProperty.Register(nameof(CommandOutput), typeof(string), typeof(EncoderListViewItem), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty TotalTimeProperty = DependencyProperty.Register(nameof(TotalTime), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero)); public static readonly DependencyProperty TimeToAnalyzeProperty = DependencyProperty.Register(nameof(TimeToAnalyze), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero, TimeSpan_PropertyChanged)); public static readonly DependencyProperty TimeToEncodeProperty = DependencyProperty.Register(nameof(TimeToEncode), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero, TimeSpan_PropertyChanged)); public static readonly DependencyProperty TimeToUploadProperty = DependencyProperty.Register(nameof(TimeToUpload), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero, TimeSpan_PropertyChanged)); public static readonly DependencyProperty TimeToCopyProperty = DependencyProperty.Register(nameof(TimeToCopy), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero, TimeSpan_PropertyChanged)); public static readonly DependencyProperty TimeToExecuteProperty = DependencyProperty.Register(nameof(TimeToExecute), typeof(TimeSpan), typeof(EncoderListViewItem), new PropertyMetadata(TimeSpan.Zero, TimeSpan_PropertyChanged)); #endregion #region Properties /// /// The icon of the ListViewItem. /// [Description("The icon of the ListViewItem.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The encoding percentage. /// [Description("The encoding percentage.")] public double Percentage { get => (double)GetValue(PercentageProperty); set => SetCurrentValue(PercentageProperty, value); } /// /// The current frame being processed. /// [Description("The frame count.")] public int CurrentFrame { get => (int)GetValue(CurrentFrameProperty); set { SetCurrentValue(CurrentFrameProperty, value); if (CurrentFrame == 0) { Percentage = 0; return; } // 100% = FrameCount // 100% * CurrentFrame / FrameCount = Actual Percentage Percentage = Math.Round(CurrentFrame * 100.0 / FrameCount, 1, MidpointRounding.AwayFromZero); } } /// /// The frame count. /// [Description("The frame count.")] public int FrameCount { get => (int)GetValue(FrameCountProperty); set => SetCurrentValue(FrameCountProperty, value); } /// /// The description of the item. /// [Description("The description of the item.")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The ID of the Task. /// [Description("The ID of the Task.")] public int Id { get => (int)GetValue(IdProperty); set => SetCurrentValue(IdProperty, value); } /// /// The Cancellation Token Source. /// [Description("The Cancellation Token Source.")] public CancellationTokenSource TokenSource { get => (CancellationTokenSource)GetValue(TokenSourceProperty); set => SetCurrentValue(TokenSourceProperty, value); } /// /// The state of the progress bar. /// [Description("The state of the progress bar.")] public bool IsIndeterminate { get => (bool)GetValue(IsIndeterminateProperty); set => SetCurrentValue(IsIndeterminateProperty, value); } /// /// The status of the encoding. /// [Description("The status of the encoding.")] public EncodingStatus Status { get => (EncodingStatus)GetValue(StatusProperty); set => SetCurrentValue(StatusProperty, value); } /// /// The size of the output file in bytes. /// [Description("The size of the output file in bytes.")] public long SizeInBytes { get => (long)GetValue(SizeInBytesProperty); set => SetCurrentValue(SizeInBytesProperty, value); } /// /// The filename of the output file. /// [Description("The filename of the output file.")] public string OutputFilename { get => (string)GetValue(OutputFilenameProperty); set => SetCurrentValue(OutputFilenameProperty, value); } /// /// The path of the output file. /// [Description("The path of the output file.")] public string OutputPath { get => (string)GetValue(OutputPathProperty); set => SetCurrentValue(OutputPathProperty, value); } /// /// True if the outfile file was saved to disk. /// [Description("True if the outfile file was saved to disk.")] public bool SavedToDisk { get => (bool)GetValue(SavedToDiskProperty); set => SetCurrentValue(SavedToDiskProperty, value); } /// /// True if the exporter exported multiple files. /// [Description("True if the exporter exported multiple files.")] public bool AreMultipleFiles { get => (bool)GetValue(AreMultipleFilesProperty); set => SetCurrentValue(AreMultipleFilesProperty, value); } /// /// The type of the output. /// [Description("The type of the output.")] public ExportFormats OutputType { get => (ExportFormats)GetValue(OutputTypeProperty); set => SetCurrentValue(OutputTypeProperty, value); } /// /// The exception of the encoding. /// [Description("The exception of the encoding.")] public Exception Exception { get => (Exception)GetValue(ExceptionProperty); set => SetCurrentValue(ExceptionProperty, value); } /// /// True if the outfile file was uploaded. /// [Description("True if the outfile file was uploaded.")] public bool Uploaded { get => (bool)GetValue(UploadedProperty); set => SetCurrentValue(UploadedProperty, value); } /// /// The link to the uploaded file. /// [Description("The link to the uploaded file.")] public string UploadLink { get => (string)GetValue(UploadLinkProperty); set => SetCurrentValue(UploadLinkProperty, value); } /// /// The link to the uploaded file (without the http). /// [Description("The link to the uploaded file (without the http).")] public string UploadLinkDisplay { get => (string)GetValue(UploadLinkDisplayProperty); set => SetCurrentValue(UploadLinkDisplayProperty, value); } /// /// The link to delete the uploaded file. /// [Description("The link to delete the uploaded file.")] public string DeletionLink { get => (string)GetValue(DeletionLinkProperty); set => SetCurrentValue(DeletionLinkProperty, value); } /// /// The exception detail about the upload task. /// [Description("The exception detail about the upload task.")] public Exception UploadTaskException { get => (Exception)GetValue(UploadTaskExceptionProperty); set => SetCurrentValue(UploadTaskExceptionProperty, value); } /// /// True if the outfile file was copied to the clipboard. /// [Description("True if the outfile file was copied to the clipboard.")] public bool CopiedToClipboard { get => (bool)GetValue(CopiedToClipboardProperty); set => SetCurrentValue(CopiedToClipboardProperty, value); } /// /// The exception detail about the copy task. /// [Description("The exception detail about the copy task.")] public Exception CopyTaskException { get => (Exception)GetValue(CopyTaskExceptionProperty); set => SetCurrentValue(CopyTaskExceptionProperty, value); } /// /// True if the post encoding commands were executed. /// [Description("True if the post encoding commands were executed.")] public bool CommandExecuted { get => (bool)GetValue(CommandExecutedProperty); set => SetCurrentValue(CommandExecutedProperty, value); } /// /// The exception detail about the post encoding command task. /// [Description("The exception detail about the post encoding command task.")] public Exception CommandTaskException { get => (Exception)GetValue(CommandTaskExceptionProperty); set => SetCurrentValue(CommandTaskExceptionProperty, value); } /// /// The command that was executed. /// [Description("The command that was executed.")] public string Command { get => (string)GetValue(CommandProperty); set => SetCurrentValue(CommandProperty, value); } /// /// The output from the post encoding commands. /// [Description("The output from the post encoding commands.")] public string CommandOutput { get => (string)GetValue(CommandOutputProperty); set => SetCurrentValue(CommandOutputProperty, value); } /// /// The total time to finish the process. /// [Description("The total time to finish the process.")] public TimeSpan TotalTime { get => (TimeSpan)GetValue(TotalTimeProperty); set => SetValue(TotalTimeProperty, value); } /// /// The time it took to analyze the frames. /// [Description("The time it took to analyze the frames.")] public TimeSpan TimeToAnalyze { get => (TimeSpan)GetValue(TimeToAnalyzeProperty); set => SetValue(TimeToAnalyzeProperty, value); } /// /// The time it took to encode the frames. /// [Description("The time it took to encode the frames.")] public TimeSpan TimeToEncode { get => (TimeSpan)GetValue(TimeToEncodeProperty); set => SetValue(TimeToEncodeProperty, value); } /// /// The time it took to upload the file. /// [Description("The time it took to upload the file.")] public TimeSpan TimeToUpload { get => (TimeSpan)GetValue(TimeToUploadProperty); set => SetValue(TimeToUploadProperty, value); } /// /// The time it took to copy the file. /// [Description("The time it took to copy the file.")] public TimeSpan TimeToCopy { get => (TimeSpan)GetValue(TimeToCopyProperty); set => SetValue(TimeToCopyProperty, value); } /// /// The time it took to execute the post encoding commands. /// [Description("The time it took to execute the post encoding commands.")] public TimeSpan TimeToExecute { get => (TimeSpan)GetValue(TimeToExecuteProperty); set => SetValue(TimeToExecuteProperty, value); } #endregion #region Custom Events public static readonly RoutedEvent CancelClickedEvent = EventManager.RegisterRoutedEvent("CancelClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EncoderListViewItem)); public static readonly RoutedEvent OpenFileClickedEvent = EventManager.RegisterRoutedEvent("OpenFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EncoderListViewItem)); public static readonly RoutedEvent ExploreFolderClickedEvent = EventManager.RegisterRoutedEvent("ExploreFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EncoderListViewItem)); /// /// Event raised when the user clicks on the cancel button. /// public event RoutedEventHandler CancelClicked { add => AddHandler(CancelClickedEvent, value); remove => RemoveHandler(CancelClickedEvent, value); } /// /// Event raised when the user clicks on the "Open file" button. /// public event RoutedEventHandler OpenFileClicked { add => AddHandler(OpenFileClickedEvent, value); remove => RemoveHandler(OpenFileClickedEvent, value); } /// /// Event raised when the user clicks on the "Explore folder" button. /// public event RoutedEventHandler ExploreFolderClicked { add => AddHandler(ExploreFolderClickedEvent, value); remove => RemoveHandler(ExploreFolderClickedEvent, value); } public void RaiseCancelClickedEvent() { if (CancelClickedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(CancelClickedEvent); RaiseEvent(newEventArgs); } public void RaiseOpenFileClickedEvent() { if (OpenFileClickedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(OpenFileClickedEvent); RaiseEvent(newEventArgs); } public void RaiseExploreFolderClickedEvent() { if (ExploreFolderClickedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(ExploreFolderClickedEvent); RaiseEvent(newEventArgs); } #endregion static EncoderListViewItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(EncoderListViewItem), new FrameworkPropertyMetadata(typeof(EncoderListViewItem))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); var cancelButton = Template.FindName("CancelButton", this) as ExtendedButton; var copyFailedHyperlink = Template.FindName("CopyFailedHyperlink", this) as Hyperlink; var executedHyperlink = Template.FindName("ExecutedHyperlink", this) as Hyperlink; var executionFailedHyperlink = Template.FindName("ExecutionFailedHyperlink", this) as Hyperlink; var uploadHyperlink = Template.FindName("UploadHyperlink", this) as Hyperlink; var uploadFailedHyperlink = Template.FindName("UploadFailedHyperlink", this) as Hyperlink; var fileButton = Template.FindName("FileButton", this) as ExtendedButton; var folderButton = Template.FindName("FolderButton", this) as ExtendedButton; var detailsButton = Template.FindName("DetailsButton", this) as ExtendedButton; var copyMenu = Template.FindName("CopyMenuItem", this) as ExtendedMenuItem; var copyImageMenu = Template.FindName("CopyImageMenuItem", this) as ExtendedMenuItem; var copyFilenameMenu = Template.FindName("CopyFilenameMenuItem", this) as ExtendedMenuItem; var copyFolderMenu = Template.FindName("CopyFolderMenuItem", this) as ExtendedMenuItem; var copyLinkMenu = Template.FindName("CopyLinkMenuItem", this) as ExtendedMenuItem; if (cancelButton != null) cancelButton.Click += (s, a) => RaiseCancelClickedEvent(); //Copy failed. if (copyFailedHyperlink != null) copyFailedHyperlink.Click += (s, a) => { if (CopyTaskException == null) return; var viewer = new ExceptionViewer(CopyTaskException); viewer.ShowDialog(); }; //Command executed. if (executedHyperlink != null) executedHyperlink.Click += (s, a) => { var dialog = new TextDialog { Command = Command, Output = CommandOutput }; dialog.ShowDialog(); }; //Command execution failed. if (executionFailedHyperlink != null) executionFailedHyperlink.Click += (s, a) => { if (CommandTaskException == null) return; var viewer = new ExceptionViewer(CommandTaskException); viewer.ShowDialog(); }; //Upload done. if (uploadHyperlink != null) uploadHyperlink.Click += (s, a) => { try { if (string.IsNullOrWhiteSpace(UploadLink)) return; ProcessHelper.StartWithShell(Keyboard.Modifiers != ModifierKeys.Control || string.IsNullOrWhiteSpace(DeletionLink) ? UploadLink : DeletionLink); } catch (Exception e) { LogWriter.Log(e, "Error while opening the upload link"); } }; //Upload failed. if (uploadFailedHyperlink != null) uploadFailedHyperlink.Click += (s, a) => { if (UploadTaskException == null) return; var viewer = new ExceptionViewer(UploadTaskException); viewer.ShowDialog(); }; //Open file. if (fileButton != null) fileButton.Click += (s, a) => { RaiseOpenFileClickedEvent(); try { if (!string.IsNullOrWhiteSpace(OutputFilename) && File.Exists(OutputFilename)) ProcessHelper.StartWithShell(OutputFilename); } catch (Exception ex) { Dialog.Ok("Open File", "Error while opening the file", ex.Message); } }; //Open folder. if (folderButton != null) folderButton.Click += (s, a) => { RaiseExploreFolderClickedEvent(); try { if (!string.IsNullOrWhiteSpace(OutputFilename) && Directory.Exists(OutputPath)) Process.Start("explorer.exe", $"/select,\"{OutputFilename.Replace("/", "\\")}\""); } catch (Exception ex) { Dialog.Ok("Explore Folder", "Error while opening the folder", ex.Message); } }; //Details. Usually when something wrong happens. if (detailsButton != null) detailsButton.Click += (s, a) => { if (Exception == null) return; var viewer = new ExceptionViewer(Exception); viewer.ShowDialog(); }; //Copy (as image and text). if (copyMenu != null) copyMenu.Click += (s, a) => { if (string.IsNullOrWhiteSpace(OutputFilename)) return; var data = new DataObject(); data.SetFileDropList(new StringCollection { OutputFilename }); SetClipboard(data); }; //Copy as image. if (copyImageMenu != null) copyImageMenu.Click += (s, a) => { if (string.IsNullOrWhiteSpace(OutputFilename)) return; var data = new DataObject(); data.SetImage(OutputFilename.SourceFrom()); SetClipboard(data); }; //Copy full path. if (copyFilenameMenu != null) copyFilenameMenu.Click += (s, a) => { if (string.IsNullOrWhiteSpace(OutputFilename)) return; var data = new DataObject(); data.SetText(OutputFilename, TextDataFormat.Text); SetClipboard(data); }; //Copy folder path. if (copyFolderMenu != null) copyFolderMenu.Click += (s, a) => { if (string.IsNullOrWhiteSpace(OutputPath)) return; var data = new DataObject(); data.SetText(OutputPath, TextDataFormat.Text); SetClipboard(data); }; // Copy link if (copyLinkMenu != null) { copyLinkMenu.Click += (s, a) => { if (string.IsNullOrWhiteSpace(UploadLink)) return; var data = new DataObject(); data.SetText(UploadLink, TextDataFormat.Text); SetClipboard(data); }; } } private static void OutputFilename_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is EncoderListViewItem item)) return; item.OutputPath = Path.GetDirectoryName(item.OutputFilename); } private static void TimeSpan_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is EncoderListViewItem item)) return; item.TotalTime = item.TimeToAnalyze + item.TimeToEncode + item.TimeToUpload + item.TimeToCopy + item.TimeToExecute; } private void SetClipboard(DataObject data) { //It tries to set the data to the clipboard 10 times before failing it to do so. //This issue may happen if the clipboard is opened by any clipboard manager. for (var i = 0; i < 10; i++) { try { Clipboard.SetDataObject(data, true); break; } catch (COMException ex) { if ((uint)ex.ErrorCode != 0x800401D0) //CLIPBRD_E_CANT_OPEN throw; } Thread.Sleep(100); } } } ================================================ FILE: ScreenToGif/Controls/ExListViewItem.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace ScreenToGif.Controls; public class ExListViewItem : ListViewItem { public static readonly DependencyProperty IsPressedProperty = DependencyProperty.Register(nameof(IsPressed), typeof(bool), typeof(ExListViewItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExListViewItem), new FrameworkPropertyMetadata()); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExListViewItem), new FrameworkPropertyMetadata(26.0)); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExListViewItem), new FrameworkPropertyMetadata(26.0)); public bool IsPressed { get => (bool)GetValue(IsPressedProperty); set => SetValue(IsPressedProperty, value); } /// /// The icon of the radio button. /// [Description("The icon of the radio button.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } static ExListViewItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExListViewItem), new FrameworkPropertyMetadata(typeof(ExListViewItem))); } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); if (IsEnabled) { IsPressed = true; CaptureMouse(); e.Handled = true; } } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnMouseLeftButtonUp(e); if (IsMouseCaptured) ReleaseMouseCapture(); IsPressed = false; } protected override void OnLostMouseCapture(MouseEventArgs e) { base.OnLostMouseCapture(e); IsPressed = false; } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); // Optional: if you want press to cancel when leaving the item if (IsMouseCaptured && !Mouse.LeftButton.HasFlag(MouseButtonState.Pressed)) IsPressed = false; } } ================================================ FILE: ScreenToGif/Controls/ExWindow.cs ================================================ using ScreenToGif.Domain.Enums.Native; using ScreenToGif.Native.External; using ScreenToGif.Native.Helpers; using ScreenToGif.Native.Structs; using ScreenToGif.Util; using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Shell; namespace ScreenToGif.Controls; [TemplatePart(Name = NonClientAreaElementId, Type = typeof(UIElement))] [TemplatePart(Name = MinimizeButtonId, Type = typeof(Button))] [TemplatePart(Name = MaximizeButtonId, Type = typeof(Button))] [TemplatePart(Name = RestoreButtonId, Type = typeof(Button))] [TemplatePart(Name = CloseButtonId, Type = typeof(Button))] public class ExWindow : Window { private const string NonClientAreaElementId = "NonClientAreaElement"; private const string MinimizeButtonId = "MinimizeButton"; private const string MaximizeButtonId = "MaximizeButton"; private const string RestoreButtonId = "RestoreButton"; private const string CloseButtonId = "CloseButton"; private UIElement _nonClientAreaElement; private Button _minimizeButton; private Button _maximizeButton; private Button _restoreButton; private Button _closeButton; public static readonly DependencyProperty ExtendIntoTitleBarProperty = DependencyProperty.Register(nameof(ExtendIntoTitleBar), typeof(bool), typeof(ExWindow), new PropertyMetadata(false)); public static readonly DependencyProperty ShowCustomCaptionButtonsProperty = DependencyProperty.Register(nameof(ShowCustomCaptionButtons), typeof(bool), typeof(ExWindow), new PropertyMetadata(true, ShowCustomCaptionButtons_PropertyChanged)); public static readonly DependencyPropertyKey WillRenderCustomCaptionButtonsProperty = DependencyProperty.RegisterReadOnly(nameof(WillRenderCustomCaptionButtons), typeof(bool), typeof(ExWindow), new PropertyMetadata(false)); public static readonly DependencyProperty ShowMinimizeButtonProperty = DependencyProperty.Register(nameof(ShowMinimizeButton), typeof(bool), typeof(ExWindow), new PropertyMetadata(true, ShowMinimizeButton_PropertyChanged)); public static readonly DependencyProperty ShowMaximizeButtonProperty = DependencyProperty.Register(nameof(ShowMaximizeButton), typeof(bool), typeof(ExWindow), new PropertyMetadata(true, ShowMaximizeButton_PropertyChanged)); public bool ExtendIntoTitleBar { get => (bool)GetValue(ExtendIntoTitleBarProperty); set => SetValue(ExtendIntoTitleBarProperty, value); } public bool ShowCustomCaptionButtons { get => (bool)GetValue(ShowCustomCaptionButtonsProperty); set => SetValue(ShowCustomCaptionButtonsProperty, value); } public bool WillRenderCustomCaptionButtons { get => (bool)GetValue(WillRenderCustomCaptionButtonsProperty.DependencyProperty); private set => SetValue(WillRenderCustomCaptionButtonsProperty, value); } public bool ShowMinimizeButton { get => (bool)GetValue(ShowMinimizeButtonProperty); set => SetValue(ShowMinimizeButtonProperty, value); } public bool ShowMaximizeButton { get => (bool)GetValue(ShowMaximizeButtonProperty); set => SetValue(ShowMaximizeButtonProperty, value); } static ExWindow() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExWindow), new FrameworkPropertyMetadata(typeof(ExWindow))); } public ExWindow() { WillRenderCustomCaptionButtons = ShowCustomCaptionButtons; var chrome = new WindowChrome { CaptionHeight = 32, ResizeBorderThickness = SystemParameters.WindowResizeBorderThickness, UseAeroCaptionButtons = !WillRenderCustomCaptionButtons, GlassFrameThickness = new Thickness(-1), NonClientFrameEdges = !WillRenderCustomCaptionButtons || ResizeMode == ResizeMode.NoResize ? NonClientFrameEdges.Right | NonClientFrameEdges.Left : NonClientFrameEdges.None }; WindowChrome.SetWindowChrome(this, chrome); } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); this.GetHwndSource()?.AddHook(Window_Hook); } public override void OnApplyTemplate() { _nonClientAreaElement = GetTemplateChild(NonClientAreaElementId) as UIElement; _minimizeButton = GetTemplateChild(MinimizeButtonId) as Button; _maximizeButton = GetTemplateChild(MaximizeButtonId) as Button; _restoreButton = GetTemplateChild(RestoreButtonId) as Button; _closeButton = GetTemplateChild(CloseButtonId) as Button; RegisterBaseCommands(); base.OnApplyTemplate(); this.SetResizeMode(); } protected void RegisterBaseCommands() { CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (_, _) => SystemCommands.MinimizeWindow(this), (_, args) => args.CanExecute = ShowMinimizeButton)); CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (_, _) => SystemCommands.MaximizeWindow(this), (_, args) => args.CanExecute = ShowMaximizeButton)); CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (_, _) => SystemCommands.RestoreWindow(this), (_, args) => args.CanExecute = ShowMaximizeButton)); CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (_, _) => SystemCommands.CloseWindow(this))); } private nint Window_Hook(nint hwnd, int msg, nint wparam, nint lparam, ref bool handled) { switch ((WindowsMessages)msg) { case WindowsMessages.NonClientHitTest: { try { //Works around a Logitech mouse driver bug, code from https://developercommunity.visualstudio.com/content/problem/167357/overflow-exception-in-windowchrome.html var _ = lparam.ToInt32(); } catch (OverflowException) { handled = true; } if (!ShowCustomCaptionButtons || !ExtendIntoTitleBar || !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) || !ShowMaximizeButton || ResizeMode is ResizeMode.NoResize or ResizeMode.CanMinimize) return nint.Zero; var x = lparam.ToInt32() & 0xffff; var y = lparam.ToInt32() >> 16; var button = WindowState == WindowState.Maximized ? _restoreButton : _maximizeButton; if (button.HitTestElement(x, y)) { button.SetCurrentValue(BackgroundProperty, FindResource("Element.Background.Hover")); handled = true; return (nint)HitTestTargets.MaximizeButton; //new nint((int)HitTestTargets.MaximizeButton); } button.ClearValue(BackgroundProperty); break; } case WindowsMessages.NonClientLeftButtonDown: { if (!ShowCustomCaptionButtons || !ExtendIntoTitleBar || !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) || !ShowMaximizeButton || ResizeMode is ResizeMode.NoResize or ResizeMode.CanMinimize) return nint.Zero; //This is necessary in order to change the background color for the maximize/restore button, since the HitTest is handled above. var x = lparam.ToInt32() & 0xffff; var y = lparam.ToInt32() >> 16; var button = WindowState == WindowState.Maximized ? _restoreButton : _maximizeButton; if (button.HitTestElement(x, y)) { button.SetCurrentValue(BackgroundProperty, FindResource("Element.Background.Pressed")); //Without this, the button click near the bottom border would not work and it would display a ghost button nearby. handled = true; } else button.ClearValue(BackgroundProperty); break; } case WindowsMessages.NonClientLeftButtonUp: { if (!ShowCustomCaptionButtons || !ExtendIntoTitleBar || !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) || !ShowMaximizeButton || ResizeMode is ResizeMode.NoResize or ResizeMode.CanMinimize) return nint.Zero; //This is necessary in order to change the background color for the maximize/restore button, since the HitTest is handled above. var x = lparam.ToInt32() & 0xffff; var y = lparam.ToInt32() >> 16; var button = WindowState == WindowState.Maximized ? _restoreButton : _maximizeButton; if (button.HitTestElement(x, y)) { button.SetCurrentValue(BackgroundProperty, FindResource("Element.Background.Pressed")); //Without this, the button click near the bottom border would not work and it would display a ghost button nearby. button.Command.Execute(null); handled = true; } else button.ClearValue(BackgroundProperty); break; } case WindowsMessages.GetMinMaxInfo: { var info = (MinMaxInfo)Marshal.PtrToStructure(lparam, typeof(MinMaxInfo))!; var monitor = WindowHelper.NearestMonitorForWindow(hwnd); if (monitor != nint.Zero) { var monitorInfo = new MonitorInfoEx(); User32.GetMonitorInfo(new HandleRef(this, monitor), monitorInfo); var rcWorkArea = monitorInfo.Work; var rcMonitorArea = monitorInfo.Monitor; //TODO: Possible issue with multi monitor setups? info.MaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left) - 1; info.MaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top) - 1; info.MaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left + 2); info.MaxSize.Y = Math.Abs(rcWorkArea.Bottom - rcWorkArea.Top + 2); } Marshal.StructureToPtr(info, lparam, true); break; } //case WindowsMessages.WindowPositionChanged: //{ // BorderThickness = WindowState == WindowState.Maximized ? new Thickness(0) : new Thickness(1); // break; //} } return nint.Zero; } private static void ShowCustomCaptionButtons_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ExWindow window) return; window.WillRenderCustomCaptionButtons = window.ShowCustomCaptionButtons; WindowChrome.GetWindowChrome(window).UseAeroCaptionButtons = !window.WillRenderCustomCaptionButtons; WindowChrome.GetWindowChrome(window).NonClientFrameEdges = !window.WillRenderCustomCaptionButtons ? NonClientFrameEdges.Right | NonClientFrameEdges.Left : NonClientFrameEdges.None; } private static void ShowMinimizeButton_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ExWindow window) return; if (window.ShowMinimizeButton) window.EnableMinimize(); else window.DisableMinimize(); } private static void ShowMaximizeButton_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ExWindow window) return; if (window.ShowMaximizeButton) window.EnableMaximize(); else window.DisableMaximize(); } } ================================================ FILE: ScreenToGif/Controls/ExtendedButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; public class ExtendedButton : Button { #region Variables public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedButton)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExtendedButton)); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedButton), new FrameworkPropertyMetadata(double.NaN)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedButton), new FrameworkPropertyMetadata(double.NaN)); public static readonly DependencyProperty KeyGestureProperty = DependencyProperty.Register(nameof(KeyGesture), typeof(string), typeof(ExtendedButton), new FrameworkPropertyMetadata("")); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties /// /// The icon of the button as a brush. /// [Description("The icon of the button as a brush."), Category("Common")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The text of the button. /// [Description("The text of the button."), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The height of the button content. /// [Description("The height of the button content."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the button content. /// [Description("The width of the button content."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The KeyGesture of the button. /// [Description("The KeyGesture of the button."), Category("Common")] public string KeyGesture { get => (string)GetValue(KeyGestureProperty); set => SetCurrentValue(KeyGestureProperty, value); } /// /// The TextWrapping property controls whether or not text wraps /// when it reaches the flow edge of its containing block box. /// public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } #endregion static ExtendedButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedButton), new FrameworkPropertyMetadata(typeof(ExtendedButton))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedCheckBox.cs ================================================ using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; public class ExtendedCheckBox : CheckBox { #region Dependency Properties public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExtendedCheckBox), new PropertyMetadata()); public static readonly DependencyProperty InfoProperty = DependencyProperty.Register(nameof(Info), typeof(string), typeof(ExtendedCheckBox), new PropertyMetadata()); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedCheckBox), new PropertyMetadata(TextWrapping.Wrap)); public static readonly DependencyProperty UncheckOnDisableProperty = DependencyProperty.Register(nameof(UncheckOnDisable), typeof(bool), typeof(ExtendedCheckBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsSmallProperty = DependencyProperty.Register(nameof(IsSmall), typeof(bool), typeof(ExtendedCheckBox), new PropertyMetadata(false)); #endregion #region Properties public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } public string Info { get => (string)GetValue(InfoProperty); set => SetValue(InfoProperty, value); } public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } public bool UncheckOnDisable { get => (bool)GetValue(UncheckOnDisableProperty); set => SetValue(UncheckOnDisableProperty, value); } public bool IsSmall { get => (bool)GetValue(IsSmallProperty); set => SetValue(IsSmallProperty, value); } #endregion #region Custom Events public static readonly RoutedEvent CheckedChangedEvent = EventManager.RegisterRoutedEvent(nameof(CheckedChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExtendedCheckBox)); public event RoutedEventHandler CheckedChanged { add => AddHandler(CheckedChangedEvent, value); remove => RemoveHandler(CheckedChangedEvent, value); } public void RaiseCheckedChangedEvent() { if (CheckedChangedEvent == null) return; var newEventArgs = new RoutedEventArgs(CheckedChangedEvent); RaiseEvent(newEventArgs); } #endregion static ExtendedCheckBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedCheckBox), new FrameworkPropertyMetadata(typeof(ExtendedCheckBox))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); Checked += (sender, args) => RaiseCheckedChangedEvent(); Unchecked += (sender, args) => RaiseCheckedChangedEvent(); if (UncheckOnDisable) IsEnabledChanged += (sender, args) => { if (!IsEnabled) IsChecked = false; }; } } ================================================ FILE: ScreenToGif/Controls/ExtendedComboBox.cs ================================================ using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; public class ExtendedComboBox : ComboBox { public static readonly DependencyProperty SelectionItemBoxProperty = DependencyProperty.Register(nameof(SelectionItemBox), typeof(object), typeof(ExtendedComboBox), new PropertyMetadata(default(object))); public static readonly DependencyProperty SelectionItemBoxTemplateProperty = DependencyProperty.Register(nameof(SelectionItemBoxTemplate), typeof(DataTemplate), typeof(ExtendedComboBox), new PropertyMetadata(default(DataTemplate))); public static readonly DependencyProperty EmptyItemProperty = DependencyProperty.Register(nameof(EmptyItem), typeof(object), typeof(ExtendedComboBox), new PropertyMetadata(default(object))); public static readonly DependencyProperty NoSelectionItemProperty = DependencyProperty.Register(nameof(NoSelectionItem), typeof(object), typeof(ExtendedComboBox), new PropertyMetadata(default(object))); public object SelectionItemBox { get => GetValue(SelectionItemBoxProperty); set => SetValue(SelectionItemBoxProperty, value); } public DataTemplate SelectionItemBoxTemplate { get => (DataTemplate) GetValue(SelectionItemBoxTemplateProperty); set => SetValue(SelectionItemBoxTemplateProperty, value); } public object EmptyItem { get => GetValue(EmptyItemProperty); set => SetValue(EmptyItemProperty, value); } public object NoSelectionItem { get => GetValue(NoSelectionItemProperty); set => SetValue(NoSelectionItemProperty, value); } static ExtendedComboBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedComboBox), new FrameworkPropertyMetadata(typeof(ExtendedComboBox))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedListBoxItem.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// ListBoxItem used by the languages listBox. /// public class ExtendedListBoxItem : ListBoxItem { #region Variables public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(UIElement), typeof(ExtendedListBoxItem)); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedListBoxItem)); public static readonly DependencyProperty MainAuthorProperty = DependencyProperty.Register(nameof(MainAuthor), typeof(string), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata("")); public static readonly DependencyProperty AuthorProperty = DependencyProperty.Register(nameof(Author), typeof(string), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata("")); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata(20d)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata(20d)); public static readonly DependencyProperty IndexProperty = DependencyProperty.Register(nameof(Index), typeof(int), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata(0)); public static readonly DependencyProperty ShowMarkOnSelectionProperty = DependencyProperty.Register(nameof(ShowMarkOnSelection), typeof(bool), typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata(true)); #endregion #region Properties /// /// The Image of the ListBoxItem. /// [Description("The Image of the ListBoxItem.")] public UIElement Image { get => (UIElement)GetValue(ImageProperty); set => SetCurrentValue(ImageProperty, value); } /// /// The icon of the ListBoxItem as a Brush. /// [Description("The icon of the ListBoxItem as a Brush.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The author of the ListBoxItem. /// [Description("The main author of the ListBoxItem.")] public string MainAuthor { get => (string)GetValue(MainAuthorProperty); set => SetCurrentValue(MainAuthorProperty, value); } /// /// The author of the ListBoxItem. /// [Description("The author of the ListBoxItem.")] public string Author { get => (string)GetValue(AuthorProperty); set => SetCurrentValue(AuthorProperty, value); } /// /// The height of the icon. /// [Description("The height of the icon."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the icon. /// [Description("The width of the icon."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The index of the item on the list. Must be manually set. /// [Description("The index of the item on the list. Must be manually set.")] public int Index { get => (int)GetValue(IndexProperty); set => SetCurrentValue(IndexProperty, value); } /// /// True if the item must show the checkmark on selection. /// [Description("True if the item must show the checkmark on selection.")] public bool ShowMarkOnSelection { get => (bool)GetValue(ShowMarkOnSelectionProperty); set => SetCurrentValue(ShowMarkOnSelectionProperty, value); } #endregion static ExtendedListBoxItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedListBoxItem), new FrameworkPropertyMetadata(typeof(ExtendedListBoxItem))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedMenuItem.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// MenuItem with an image to the left. /// public class ExtendedMenuItem : MenuItem { #region Variables public new static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(Icon_Changed)); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(16d)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(16d)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty HasIconProperty = DependencyProperty.Register(nameof(HasIcon), typeof(bool), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty DarkModeProperty = DependencyProperty.Register(nameof(DarkMode), typeof(bool), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IsOverNonClientAreaProperty = DependencyProperty.Register(nameof(IsOverNonClientArea), typeof(bool), typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(false)); #endregion #region Properties /// /// The icon of the button as a Brush. /// [Description("The icon of the button as a Brush.")] public new Brush Icon { get => (Brush)GetValue(IconProperty); set { SetCurrentValue(IconProperty, value); SetCurrentValue(HasIconProperty, value != null); } } /// /// The height of the button content. /// [Description("The height of the button content."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the button content. /// [Description("The width of the button content."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetCurrentValue(TextWrappingProperty, value); } /// /// True if the menu item contains an icon. /// [Description("True if the menu item contains an icon.")] public bool HasIcon { get => (bool)GetValue(HasIconProperty); set => SetCurrentValue(HasIconProperty, value); } /// /// True if the menu should adjust itself for dark mode. /// [Description("True if the menu should adjust itself for dark mode.")] public bool DarkMode { get => (bool)GetValue(DarkModeProperty); set => SetCurrentValue(DarkModeProperty, value); } /// /// True if the button is being drawn on top of the non client area. /// [Description("True if the button is being drawn on top of the non client area.")] public bool IsOverNonClientArea { get => (bool)GetValue(IsOverNonClientAreaProperty); set => SetCurrentValue(IsOverNonClientAreaProperty, value); } #endregion #region Property Changed private static void Icon_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ExtendedMenuItem)d).HasIcon = e.NewValue != null; } #endregion static ExtendedMenuItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedMenuItem), new FrameworkPropertyMetadata(typeof(ExtendedMenuItem))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedProgressBar.cs ================================================ using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; public class ExtendedProgressBar : ProgressBar { public enum ProgressState { Primary, Info, Warning, Danger } public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(ProgressState), typeof(ExtendedProgressBar), new PropertyMetadata(ProgressState.Primary)); public static readonly DependencyProperty ShowPercentageProperty = DependencyProperty.Register(nameof(ShowPercentage), typeof(bool), typeof(ExtendedProgressBar), new PropertyMetadata(default(bool))); public ProgressState State { get => (ProgressState)GetValue(StateProperty); set => SetValue(StateProperty, value); } public bool ShowPercentage { get => (bool) GetValue(ShowPercentageProperty); set => SetValue(ShowPercentageProperty, value); } static ExtendedProgressBar() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedProgressBar), new FrameworkPropertyMetadata(typeof(ExtendedProgressBar))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedRadioButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace ScreenToGif.Controls; public class ExtendedRadioButton : RadioButton { #region Variables public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedRadioButton), new FrameworkPropertyMetadata()); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExtendedRadioButton), new FrameworkPropertyMetadata()); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedRadioButton), new FrameworkPropertyMetadata(26.0)); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedRadioButton), new FrameworkPropertyMetadata(26.0)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedRadioButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties /// /// The icon of the radio button. /// [Description("The icon of the radio button.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The text of the button. /// [Description("The text of the button.")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The TextWrapping property controls whether or not text wraps /// when it reaches the flow edge of its containing block box. /// public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } #endregion static ExtendedRadioButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedRadioButton), new FrameworkPropertyMetadata(typeof(ExtendedRadioButton))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedRepeatButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace ScreenToGif.Controls; public class ExtendedRepeatButton : RepeatButton { #region Variables public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedRepeatButton)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExtendedRepeatButton), new FrameworkPropertyMetadata()); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedRepeatButton), new FrameworkPropertyMetadata(double.NaN)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedRepeatButton), new FrameworkPropertyMetadata(double.NaN)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedRepeatButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties /// /// The icon of the button as a brush. /// [Description("The icon of the button as a brush."), Category("Common")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The text of the button. /// [Description("The text of the button."), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The height of the button content. /// [Description("The height of the button content."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the button content. /// [Description("The width of the button content."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The TextWrapping property controls whether or not text wraps /// when it reaches the flow edge of its containing block box. /// public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } #endregion static ExtendedRepeatButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedRepeatButton), new FrameworkPropertyMetadata(typeof(ExtendedRepeatButton))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedSlider.cs ================================================ using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; internal class ExtendedSlider : Slider { public static readonly DependencyProperty ShowNumbersProperty = DependencyProperty.Register(nameof(ShowNumbers), typeof(bool), typeof(ExtendedSlider), new PropertyMetadata(default(bool))); public bool ShowNumbers { get => (bool) GetValue(ShowNumbersProperty); set => SetValue(ShowNumbersProperty, value); } } ================================================ FILE: ScreenToGif/Controls/ExtendedTextBox.cs ================================================ using System.ComponentModel; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; public class ExtendedTextBox : TextBox { #region Dependency Properties public static readonly DependencyProperty AllowSpacingyProperty = DependencyProperty.Register(nameof(AllowSpacing), typeof(bool), typeof(ExtendedTextBox), new PropertyMetadata(true)); public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(ExtendedTextBox), new PropertyMetadata("")); public static readonly DependencyProperty IsObligatoryProperty = DependencyProperty.Register(nameof(IsObligatory), typeof(bool), typeof(ExtendedTextBox)); public static readonly DependencyProperty AllowedCharactersProperty = DependencyProperty.Register(nameof(AllowedCharacters), typeof(string), typeof(ExtendedTextBox)); #endregion #region Properties [Bindable(true), Category("Common")] public bool AllowSpacing { get => (bool)GetValue(AllowSpacingyProperty); set => SetValue(AllowSpacingyProperty, value); } [Bindable(true), Category("Common")] public string Watermark { get => (string)GetValue(WatermarkProperty); set => SetValue(WatermarkProperty, value); } [Bindable(true), Category("Common")] public bool IsObligatory { get => (bool)GetValue(IsObligatoryProperty); set => SetValue(IsObligatoryProperty, value); } /// /// When this property has any character, the input text will be only accepted if the character is present in the list of allowed chars. /// [Bindable(true), Category("Common")] public string AllowedCharacters { get => (string)GetValue(AllowedCharactersProperty); set => SetValue(AllowedCharactersProperty, value); } #endregion static ExtendedTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedTextBox), new FrameworkPropertyMetadata(typeof(ExtendedTextBox))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); } protected override void OnPreviewKeyDown(KeyEventArgs e) { if (!AllowSpacing && e.Key == Key.Space) { e.Handled = true; return; } base.OnPreviewKeyDown(e); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (!string.IsNullOrWhiteSpace(AllowedCharacters) && !IsEntryAllowed(e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } if (UserSettings.All.TripleClickSelection && e.ClickCount == 3) SelectAll(); } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (!UserSettings.All.TripleClickSelection) SelectAll(); } private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!string.IsNullOrWhiteSpace(AllowedCharacters) && !IsTextAllowed(text)) e.CancelCommand(); return; } e.CancelCommand(); } private bool IsEntryAllowed(string text) { //Only the allowed chars. var regex = new Regex($"^[{AllowedCharacters.Replace("-", @"\-")}]+$"); //Checks if it's a valid char based on the context. return regex.IsMatch(text); } private bool IsTextAllowed(string text) { return Regex.IsMatch(text, $"^[{AllowedCharacters.Replace("-", @"\-") + (AllowSpacing ? " " : "")})]+$"); } public bool IsNullOrWhiteSpace() { return string.IsNullOrWhiteSpace(Text); } public bool IsNullOrEmpty() { return string.IsNullOrEmpty(Text); } public string Trim() { return Text.Trim(); } } ================================================ FILE: ScreenToGif/Controls/ExtendedToggleButton.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// A toggle button with a image inside. /// public class ExtendedToggleButton : ToggleButton { #region Variables public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata("Button")); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata()); public static readonly DependencyProperty KeyGestureProperty = DependencyProperty.Register(nameof(KeyGesture), typeof(string), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata("")); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(double.NaN)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(double.NaN)); /// /// DependencyProperty for property. /// public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty DarkModeProperty = DependencyProperty.Register(nameof(DarkMode), typeof(bool), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IsOverNonClientAreaProperty = DependencyProperty.Register(nameof(IsOverNonClientArea), typeof(bool), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IsImportantProperty = DependencyProperty.Register(nameof(IsImportant), typeof(bool), typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(false)); #endregion #region Properties /// /// The text of the button. /// [Description("The text of the button."), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The icon of the radio button. /// [Description("The icon of the toggle button.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The KeyGesture of the button. /// [Description("The KeyGesture of the button."), Category("Common")] public string KeyGesture { get => (string)GetValue(KeyGestureProperty); set => SetCurrentValue(KeyGestureProperty, value); } /// /// The height of the button content. /// [Description("The height of the button content."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the button content. /// [Description("The width of the button content."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The TextWrapping property controls whether or not text wraps /// when it reaches the flow edge of its containing block box. /// public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } /// /// True if the button should adjust itself for dark mode. /// [Description("True if the button should adjust itself for dark mode.")] public bool DarkMode { get => (bool)GetValue(DarkModeProperty); set => SetCurrentValue(DarkModeProperty, value); } /// /// True if the button is being drawn on top of the non client area. /// [Description("True if the button is being drawn on top of the non client area.")] public bool IsOverNonClientArea { get => (bool)GetValue(IsOverNonClientAreaProperty); set => SetCurrentValue(IsOverNonClientAreaProperty, value); } /// /// True if the button should be displayed with a warning color. /// [Description("True if the button should be displayed with a warning color.")] public bool IsImportant { get => (bool)GetValue(IsImportantProperty); set => SetCurrentValue(IsImportantProperty, value); } #endregion static ExtendedToggleButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedToggleButton), new FrameworkPropertyMetadata(typeof(ExtendedToggleButton))); } } ================================================ FILE: ScreenToGif/Controls/ExtendedUniformGrid.cs ================================================ using System; using System.Windows; using System.Windows.Controls.Primitives; namespace ScreenToGif.Controls; public class ExtendedUniformGrid : UniformGrid { public static readonly DependencyProperty IsReversedProperty = DependencyProperty.Register(nameof(IsReversed), typeof(bool), typeof(ExtendedUniformGrid), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); //private static void IsReversed_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) //{ // if (!(o is ExtendedUniformGrid grid)) // return; //} public bool IsReversed { get => (bool)GetValue(IsReversedProperty); set => SetValue(IsReversedProperty, value); } protected override Size MeasureOverride(Size constraint) { UpdateComputedValues(); var availableSize = new Size(constraint.Width / Columns, constraint.Height / Rows); var num1 = 0.0; var num2 = 0.0; if (IsReversed) { for (var i = InternalChildren.Count - 1; i >= 0; i--) { var internalChild = InternalChildren[i]; internalChild.Measure(availableSize); var desiredSize = internalChild.DesiredSize; if (num1 < desiredSize.Width) num1 = desiredSize.Width; if (num2 < desiredSize.Height) num2 = desiredSize.Height; } return new Size(num1 * Columns, num2 * Rows); } var index = 0; for (var count = InternalChildren.Count; index < count; ++index) { var internalChild = InternalChildren[index]; internalChild.Measure(availableSize); var desiredSize = internalChild.DesiredSize; if (num1 < desiredSize.Width) num1 = desiredSize.Width; if (num2 < desiredSize.Height) num2 = desiredSize.Height; } return new Size(num1 * Columns, num2 * Rows); } protected override Size ArrangeOverride(Size arrangeSize) { var finalRect = new Rect(0.0, 0.0, arrangeSize.Width / Columns, arrangeSize.Height / Rows); var width = finalRect.Width; var num = arrangeSize.Width - 1.0; finalRect.X += finalRect.Width * FirstColumn; if (IsReversed) { for (var i = InternalChildren.Count - 1; i >= 0; i--) { InternalChildren[i].Arrange(finalRect); if (InternalChildren[i].Visibility != Visibility.Collapsed) { finalRect.X += width; if (finalRect.X >= num) { finalRect.Y += finalRect.Height; finalRect.X = 0.0; } } } return arrangeSize; } foreach (UIElement internalChild in InternalChildren) { internalChild.Arrange(finalRect); if (internalChild.Visibility != Visibility.Collapsed) { finalRect.X += width; if (finalRect.X >= num) { finalRect.Y += finalRect.Height; finalRect.X = 0.0; } } } return arrangeSize; } private void UpdateComputedValues() { if (FirstColumn >= Columns) FirstColumn = 0; if (Rows != 0 && Columns != 0) return; var num = 0; var index = 0; for (var count = InternalChildren.Count; index < count; ++index) { if (InternalChildren[index].Visibility != Visibility.Collapsed) ++num; } if (num == 0) num = 1; if (Rows == 0) { if (Columns > 0) { Rows = (num + FirstColumn + (Columns - 1)) / Columns; } else { Rows = (int)Math.Sqrt(num); if (Rows * Rows < num) Rows = Rows + 1; Columns = Rows; } } else { if (Columns != 0) return; Columns = (num + (Rows - 1)) / Rows; } } } ================================================ FILE: ScreenToGif/Controls/FolderSelector.cs ================================================ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows.Forms; namespace ScreenToGif.Controls; /// /// Folder selector, vista-style. /// /// /// Source: /// https://www.magnumdb.com/search?q=IShellItem /// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifiledialog /// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions /// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-sigdn /// internal class FolderSelector { #region Native #region Constants /// /// Present an Open dialog that offers a choice of folders rather than files. /// public const uint DialogPickFolders = 0x00000020; /// /// Ensures that returned items are file system items (SFGAO_FILESYSTEM). /// Note that this does not apply to items returned by IFileDialog::GetCurrentSelection. /// public const uint DialogForceFileSystem = 0x00000040; /// /// Do not check for situations that would prevent an application from opening the selected file, such as sharing violations or access denied errors. /// public const uint DialogNoValidade = 0x00000100; /// /// Do not test whether creation of the item as specified in the Save dialog will be successful. /// If this flag is not set, the calling application must handle errors, such as denial of access, discovered when the item is created. /// public const uint DialogNoTestFileCreate = 0x00010000; /// /// Do not add the item being opened or saved to the recent documents list (SHAddToRecentDocs). /// public const uint DialogDontAddToRecent = 0x02000000; /// /// Ok return status. /// public const uint StatusOk = 0x0000; /// /// Returns the item's file system path, if it has one. Only items that report SFGAO_FILESYSTEM have a file system path. /// When an item does not have a file system path, a call to IShellItem::GetDisplayName on that item will fail. /// In UI this name is suitable for display to the user in some cases, but note that it might not be specified for all items. /// public const uint DisplayFileSysPath = 0x80058000; #endregion #region COM Imports [ComImport, ClassInterface(ClassInterfaceType.None), TypeLibType(TypeLibTypeFlags.FCanCreate), Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] internal class FileOpenDialogRCW { } [ComImport, Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileDialog { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), PreserveSig] uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFileTypeIndex([In] uint iFileType); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetFileTypeIndex(out uint piFileType); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint Unadvise([In] uint dwCookie); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetOptions([In] uint fos); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetOptions(out uint fos); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint Close([MarshalAs(UnmanagedType.Error)] uint hr); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetClientGuid([In] ref Guid guid); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint ClearClientData(); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); } [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItem { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } #endregion [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); #endregion #region Properties /// /// Gets or sets the descriptive text displayed above the tree view control in the dialog box. /// public string Description { get; set; } /// /// Gets or sets the selected folder path. /// If some path is set before opening the dialog, that path is used as a folder that is always /// selected when the dialog is opened, regardless of previous user action. /// public string SelectedPath { get; set; } /// /// Default folder to be used if no recent folder available. /// public string DefaultFolder { get; set; } #endregion public bool ShowDialog(IWin32Window owner = null) { //ReSharper disable once SuspiciousTypeConversion.Global var frm = (IFileDialog) new FileOpenDialogRCW(); //Set folder picker options. frm.GetOptions(out var options); options |= DialogPickFolders | DialogForceFileSystem | DialogNoValidade | DialogNoTestFileCreate | DialogDontAddToRecent; frm.SetOptions(options); if (!string.IsNullOrWhiteSpace(Description)) frm.SetTitle(Description); if (SelectedPath != null) { //IShellItem var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); if (SHCreateItemFromParsingName(SelectedPath, IntPtr.Zero, ref riid, out var directoryShellItem) == StatusOk) frm.SetFolder(directoryShellItem); } if (DefaultFolder != null) { //IShellItem var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); if (SHCreateItemFromParsingName(DefaultFolder, IntPtr.Zero, ref riid, out var directoryShellItem) == StatusOk) frm.SetDefaultFolder(directoryShellItem); } if (frm.Show(owner?.Handle ?? IntPtr.Zero) != StatusOk || frm.GetResult(out var shellItem) != StatusOk || shellItem.GetDisplayName(DisplayFileSysPath, out var pszString) != StatusOk || pszString == IntPtr.Zero) return false; try { SelectedPath = Marshal.PtrToStringAuto(pszString); return true; } finally { Marshal.FreeCoTaskMem(pszString); } } } ================================================ FILE: ScreenToGif/Controls/FrameViewer.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Imaging; namespace ScreenToGif.Controls; /// /// Frame viewer that works with a WriteableBitmap. /// public class FrameViewer : Control { //UI //Image scale difference with screen scale. //Zoom. //Mouse and keyboard events. //Check if rendering works. #region Properties public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(WriteableBitmap), typeof(FrameViewer), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, Source_PropertyChanged)); public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register(nameof(Zoom), typeof(double), typeof(FrameViewer), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender, Zoom_PropertyChanged)); /// /// The source image. /// [Description("The source image.")] public WriteableBitmap Source { get => (WriteableBitmap)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } /// /// The zoom level of the image. /// [Description("The zoom level of the image.")] public double Zoom { get => (double)GetValue(ZoomProperty); set => SetCurrentValue(ZoomProperty, value); } #endregion static FrameViewer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(FrameViewer), new FrameworkPropertyMetadata(typeof(FrameViewer))); } private static void Source_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //When the image source changes, the UI needs to be adjusted somewhow. } private static void Zoom_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { } } ================================================ FILE: ScreenToGif/Controls/FrameworkElementAdorner.cs ================================================ using System.Collections; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls; /// /// This class is an adorner that allows a FrameworkElement derived class to adorn another FrameworkElement. From /// public class FrameworkElementAdorner : Adorner { #region Variables and Properties /// /// The framework element that is the adorner. /// private readonly FrameworkElement _child; /// /// Placement of the child. /// private readonly AdornerPlacement _horizontalAdornerPlacement = AdornerPlacement.Inside; private readonly AdornerPlacement _verticalAdornerPlacement = AdornerPlacement.Inside; /// /// Offset of the child. /// private readonly double _offsetX = 0.0; private readonly double _offsetY = 0.0; /// /// Position of the child (when not set to NaN). /// public double PositionX { get; set; } = double.NaN; public double PositionY { get; set; } = double.NaN; #endregion public FrameworkElementAdorner(FrameworkElement adornerChildElement, FrameworkElement adornedElement) : base(adornedElement) { _child = adornerChildElement; AddLogicalChild(adornerChildElement); AddVisualChild(adornerChildElement); } public FrameworkElementAdorner(FrameworkElement adornerChildElement, FrameworkElement adornedElement, AdornerPlacement horizontalAdornerPlacement, AdornerPlacement verticalAdornerPlacement, double offsetX, double offsetY) : base(adornedElement) { _child = adornerChildElement; _horizontalAdornerPlacement = horizontalAdornerPlacement; _verticalAdornerPlacement = verticalAdornerPlacement; _offsetX = offsetX; _offsetY = offsetY; adornedElement.SizeChanged += AdornedElement_SizeChanged; AddLogicalChild(adornerChildElement); AddVisualChild(adornerChildElement); } /// /// Event raised when the adorned control's size has changed. /// private void AdornedElement_SizeChanged(object sender, SizeChangedEventArgs e) { InvalidateMeasure(); } #region Overrides protected override Size MeasureOverride(Size constraint) { _child.Measure(constraint); return _child.DesiredSize; } protected override Size ArrangeOverride(Size finalSize) { var x = PositionX; if (double.IsNaN(x)) x = DetermineX(); var y = PositionY; if (double.IsNaN(y)) y = DetermineY(); var adornerWidth = DetermineWidth(); var adornerHeight = DetermineHeight(); _child.Arrange(new Rect(x, y, adornerWidth, adornerHeight)); return finalSize; } protected override int VisualChildrenCount { get; } = 1; protected override Visual GetVisualChild(int index) { return _child; } protected override IEnumerator LogicalChildren { get { var list = new ArrayList { _child }; return (IEnumerator)list.GetEnumerator(); } } /// /// Override AdornedElement from base class for less type-checking. /// public new FrameworkElement AdornedElement => (FrameworkElement)base.AdornedElement; #endregion #region Methods /// /// Determine the X coordinate of the child. /// private double DetermineX() { switch (_child.HorizontalAlignment) { case HorizontalAlignment.Left: { if (_horizontalAdornerPlacement == AdornerPlacement.Outside) return -_child.DesiredSize.Width + _offsetX; return _offsetX; } case HorizontalAlignment.Right: { if (_horizontalAdornerPlacement == AdornerPlacement.Outside) { var adornedWidth = AdornedElement.ActualWidth; return adornedWidth + _offsetX; } else { var adornerWidth = _child.DesiredSize.Width; var adornedWidth = AdornedElement.ActualWidth; var x = adornedWidth - adornerWidth; return x + _offsetX; } } case HorizontalAlignment.Center: { var adornerWidth = _child.DesiredSize.Width; var adornedWidth = AdornedElement.ActualWidth; var x = (adornedWidth / 2) - (adornerWidth / 2); return x + _offsetX; } case HorizontalAlignment.Stretch: return 0.0; } return 0.0; } /// /// Determine the Y coordinate of the child. /// private double DetermineY() { switch (_child.VerticalAlignment) { case VerticalAlignment.Top: { if (_verticalAdornerPlacement == AdornerPlacement.Outside) return -_child.DesiredSize.Height + _offsetY; return _offsetY; } case VerticalAlignment.Bottom: { if (_verticalAdornerPlacement == AdornerPlacement.Outside) { var adornedHeight = AdornedElement.ActualHeight; return adornedHeight + _offsetY; } else { var adornerHeight = _child.DesiredSize.Height; var adornedHeight = AdornedElement.ActualHeight; var x = adornedHeight - adornerHeight; return x + _offsetY; } } case VerticalAlignment.Center: { var adornerHeight = _child.DesiredSize.Height; var adornedHeight = AdornedElement.ActualHeight; var x = (adornedHeight / 2) - (adornerHeight / 2); return x + _offsetY; } case VerticalAlignment.Stretch: return 0.0; } return 0.0; } /// /// Determine the width of the child. /// private double DetermineWidth() { if (!double.IsNaN(PositionX)) return _child.DesiredSize.Width; switch (_child.HorizontalAlignment) { case HorizontalAlignment.Left: return _child.DesiredSize.Width; case HorizontalAlignment.Right: return _child.DesiredSize.Width; case HorizontalAlignment.Center: return _child.DesiredSize.Width; case HorizontalAlignment.Stretch: return AdornedElement.ActualWidth; } return 0.0; } /// /// Determine the height of the child. /// private double DetermineHeight() { if (!double.IsNaN(PositionY)) return _child.DesiredSize.Height; switch (_child.VerticalAlignment) { case VerticalAlignment.Top: return _child.DesiredSize.Height; case VerticalAlignment.Bottom: return _child.DesiredSize.Height; case VerticalAlignment.Center: return _child.DesiredSize.Height; case VerticalAlignment.Stretch: return AdornedElement.ActualHeight; } return 0.0; } /// /// Disconnect the child element from the visual tree so that it may be reused later. /// public void DisconnectChild() { RemoveLogicalChild(_child); RemoveVisualChild(_child); } #endregion } ================================================ FILE: ScreenToGif/Controls/HeaderedTooltip.cs ================================================ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Markup; using System.Windows.Media; namespace ScreenToGif.Controls; [ContentProperty("Inlines")] [TemplatePart(Name = "PART_InlinesPresenter", Type = typeof(TextBlock))] public class HeaderedTooltip : ToolTip { #region Variables public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(nameof(Header), typeof(string), typeof(HeaderedTooltip), new FrameworkPropertyMetadata("Header")); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(HeaderedTooltip), new FrameworkPropertyMetadata("")); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register(nameof(TextAlignment), typeof(TextAlignment), typeof(HeaderedTooltip), new FrameworkPropertyMetadata(TextAlignment.Left)); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(HeaderedTooltip)); public static readonly DependencyProperty MaxSizeProperty = DependencyProperty.Register(nameof(MaxSize), typeof(double), typeof(HeaderedTooltip), new FrameworkPropertyMetadata(14.0)); private Collection _inlines = new Collection(); private TextBlock _inlinesPresenter = null; #endregion #region Properties /// /// The header of the tooltip. /// [Description("The header of the tooltip.")] public string Header { get => (string)GetValue(HeaderProperty); set => SetCurrentValue(HeaderProperty, value); } /// /// The text of the description. /// [Description("The text of the description.")] public string Text { get => (string)GetValue(TextProperty); set => SetCurrentValue(TextProperty, value); } /// /// The text alignment of the description. /// [Description("The text alignment of the description.")] public TextAlignment TextAlignment { get => (TextAlignment)GetValue(TextAlignmentProperty); set => SetCurrentValue(TextAlignmentProperty, value); } /// /// The icon of the Tooltip. /// [Description("The icon of the Tooltip.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The maximum size of the image. /// [Description("The maximum size of the image.")] public double MaxSize { get => (double)GetValue(MaxSizeProperty); set => SetCurrentValue(MaxSizeProperty, value); } public Collection Inlines { get => _inlines; set { _inlines = value; UpdateInlines(); } } #endregion static HeaderedTooltip() { DefaultStyleKeyProperty.OverrideMetadata(typeof(HeaderedTooltip), new FrameworkPropertyMetadata(typeof(HeaderedTooltip))); } public override void OnApplyTemplate() { base.ApplyTemplate(); _inlinesPresenter = GetTemplateChild("PART_InlinesPresenter") as TextBlock; if (_inlinesPresenter == null || !Inlines.Any()) return; Text = ""; var targetInlines = _inlinesPresenter.Inlines; foreach (var inline in Inlines) targetInlines.Add(inline); } public void Clear() { Text = ""; Inlines.Clear(); } public void UpdateInlines() { if (_inlinesPresenter == null) return; _inlinesPresenter.Inlines.Clear(); _inlinesPresenter.Inlines.AddRange(Inlines); } } ================================================ FILE: ScreenToGif/Controls/HexadecimalBox.cs ================================================ using System; using System.Linq; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; public class HexadecimalBox : ExtendedTextBox { #region Dependency Properties public static readonly DependencyProperty RedProperty = DependencyProperty.Register("Red", typeof(int), typeof(HexadecimalBox), new PropertyMetadata(0, Value_PropertyChanged)); public static readonly DependencyProperty GreenProperty = DependencyProperty.Register("Green", typeof(int), typeof(HexadecimalBox), new PropertyMetadata(0, Value_PropertyChanged)); public static readonly DependencyProperty BlueProperty = DependencyProperty.Register("Blue", typeof(int), typeof(HexadecimalBox), new PropertyMetadata(0, Value_PropertyChanged)); public static readonly DependencyProperty AlphaProperty = DependencyProperty.Register("Alpha", typeof(int), typeof(HexadecimalBox), new PropertyMetadata(255, Value_PropertyChanged)); public static readonly DependencyProperty DisplayGlyphProperty = DependencyProperty.Register("DisplayGlyph", typeof(bool), typeof(HexadecimalBox), new PropertyMetadata(true)); public static readonly DependencyProperty DisplayAlphaProperty = DependencyProperty.Register("DisplayAlpha", typeof(bool), typeof(HexadecimalBox), new PropertyMetadata(true)); public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HexadecimalBox)); #endregion #region Properties public int Red { get => (int)GetValue(RedProperty); set => SetValue(RedProperty, value); } public int Blue { get => (int)GetValue(BlueProperty); set => SetValue(BlueProperty, value); } public int Green { get => (int)GetValue(GreenProperty); set => SetValue(GreenProperty, value); } public int Alpha { get => (int)GetValue(AlphaProperty); set => SetValue(AlphaProperty, value); } public bool DisplayGlyph { get => (bool)GetValue(DisplayGlyphProperty); set => SetValue(DisplayGlyphProperty, value); } public bool DisplayAlpha { get => (bool)GetValue(DisplayAlphaProperty); set => SetValue(DisplayAlphaProperty, value); } public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } #endregion private static void Value_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var hexaBox = o as HexadecimalBox; if (hexaBox == null) return; hexaBox.RaiseValueChangedEvent(); hexaBox.Text = $"{(hexaBox.DisplayGlyph ? "#" : "")}{(hexaBox.DisplayAlpha ? hexaBox.Alpha.ToString("X2") : "")}{hexaBox.Red:X2}{hexaBox.Green:X2}{hexaBox.Blue:X2}"; } static HexadecimalBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(HexadecimalBox), new FrameworkPropertyMetadata(typeof(HexadecimalBox))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); Text = $"{(DisplayGlyph ? "#" : "")}{(DisplayAlpha ? Alpha.ToString("X2") : "")}{Red:X2}{Green:X2}{Blue:X2}"; } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); SelectAll(); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (string.IsNullOrEmpty(e.Text)) { e.Handled = true; return; } if (!IsEntryAllowed(this, e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnTextChanged(TextChangedEventArgs e) { if (string.IsNullOrEmpty(Text)) return; if (!IsTextAllowed(Text)) return; base.OnTextChanged(e); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) { Alpha = 255; Red = 0; Green = 0; Blue = 0; Text = $"{(DisplayGlyph ? "#" : "")}{(DisplayAlpha ? Alpha.ToString("X2") : "")}{Red:X2}{Green:X2}{Blue:X2}"; return; } #region Try parse try { var source = Text.Replace("#", ""); switch (source.Length) { case 2: Alpha = 255; Blue = Green = Red = Convert.ToInt32(source.Substring(0, 2), 16); break; case 4: Alpha = Convert.ToInt32(source.Substring(0, 2), 16); Blue = Green = Red = Convert.ToInt32(source.Substring(2, 2), 16); break; case 6: Alpha = 255; Red = Convert.ToInt32(source.Substring(0, 2), 16); Green = Convert.ToInt32(source.Substring(2, 2), 16); Blue = Convert.ToInt32(source.Substring(4, 2), 16); break; case 8: Alpha = Convert.ToInt32(source.Substring(0, 2), 16); Red = Convert.ToInt32(source.Substring(2, 2), 16); Green = Convert.ToInt32(source.Substring(4, 2), 16); Blue = Convert.ToInt32(source.Substring(6, 2), 16); break; } } catch {} #endregion Text = $"{(DisplayGlyph ? "#" : "")}{(DisplayAlpha ? Alpha.ToString("X2") : "")}{Red:X2}{Green:X2}{Blue:X2}"; } #endregion #region Base Properties Changed private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!IsTextAllowed(text)) e.CancelCommand(); } else { e.CancelCommand(); } } #endregion #region Methods void RaiseValueChangedEvent() { var newEventArgs = new RoutedEventArgs(ValueChangedEvent); RaiseEvent(newEventArgs); } private bool IsEntryAllowed(TextBox textBox, string text) { //Digits, points or commas. var regex = new Regex(@"^#|[0-9]|[A-F]|$"); //Checks if it's a valid char based on the context. return regex.IsMatch(text) && IsEntryAllowedInContext(textBox, text); } private bool IsEntryAllowedInContext(TextBox textBox, string next) { if (textBox.Text.Replace("#", "").Length > 7 && textBox.SelectionLength == 0) return false; var nChar = next.ToCharArray().FirstOrDefault(); if (char.IsNumber(nChar) || (nChar >= 97 && nChar <= 102)) //0 to 9, A to F { if (textBox.Text.Contains("#") && textBox.SelectionStart == 0) return false; return true; } if (nChar == '#') { if (textBox.Text.Any(x => x.Equals('#'))) return false; if (textBox.SelectionStart != 0) return false; return true; } return true; } private bool IsTextAllowed(string text) { //Allows: #FF, #FF11, #FF1122, #FF112233 return Regex.IsMatch(text, @"^#{0,1}(([0-9a-fA-F]{2}){1,4})$"); } #endregion } ================================================ FILE: ScreenToGif/Controls/HideableTabControl.cs ================================================ using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using ScreenToGif.Domain.Enums; using ScreenToGif.Util; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; /// /// Basic class of a Hideable TabControl. /// public class HideableTabControl : TabControl { #region Variables private Button _hideButton; private ExtendedMenuItem _extrasMenuItem; private TabPanel _tabPanel; private Border _border; private ExtendedToggleButton _notificationButton; private NotificationBox _notificationBox; #endregion #region Dependency Properties public static DependencyProperty OptionsCommandProperty = DependencyProperty.Register("OptionsCommand", typeof(ICommand), typeof(HideableTabControl), new PropertyMetadata(null)); public static DependencyProperty FeedbackCommandProperty = DependencyProperty.Register("FeedbackCommand", typeof(ICommand), typeof(HideableTabControl), new PropertyMetadata(null)); public static DependencyProperty TroubleshootCommandProperty = DependencyProperty.Register("TroubleshootCommand", typeof(ICommand), typeof(HideableTabControl), new PropertyMetadata(null)); public static DependencyProperty HelpCommandProperty = DependencyProperty.Register("HelpCommand", typeof(ICommand), typeof(HideableTabControl), new PropertyMetadata(null)); #endregion #region Properties public ICommand OptionsCommand { get => (ICommand)GetValue(OptionsCommandProperty); set => SetValue(OptionsCommandProperty, value); } public ICommand FeedbackCommand { get => (ICommand)GetValue(FeedbackCommandProperty); set => SetValue(FeedbackCommandProperty, value); } public ICommand TroubleshootCommand { get => (ICommand)GetValue(TroubleshootCommandProperty); set => SetValue(TroubleshootCommandProperty, value); } public ICommand HelpCommand { get => (ICommand)GetValue(HelpCommandProperty); set => SetValue(HelpCommandProperty, value); } #endregion static HideableTabControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(HideableTabControl), new FrameworkPropertyMetadata(typeof(HideableTabControl))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _tabPanel = Template.FindName("TabPanel", this) as TabPanel; _border = Template.FindName("ContentBorder", this) as Border; _notificationButton = Template.FindName("NotificationsButton", this) as ExtendedToggleButton; _notificationBox = Template.FindName("NotificationBox", this) as NotificationBox; _extrasMenuItem = Template.FindName("ExtrasMenuItem", this) as ExtendedMenuItem; _hideButton = Template.FindName("HideGridButton", this) as Button; //Hide button. if (_hideButton != null) _hideButton.Click += HideButton_Clicked; //Show tab (if hidden). if (_tabPanel != null) { foreach (TabItem tabItem in _tabPanel.Children) tabItem.PreviewMouseDown += TabItem_PreviewMouseDown; _tabPanel.PreviewMouseWheel += TabControl_PreviewMouseWheel; } if (_notificationButton != null) _notificationButton.Checked += NotificationButton_Checked; UpdateVisual(); AnimateOrNot(); } #region Events private void TabControl_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Delta > 0) { if (SelectedIndex < Items.Count - 1) SelectedIndex++; else SelectedIndex = 0; } else { if (SelectedIndex > 0) SelectedIndex--; else SelectedIndex = Items.Count - 1; } if (!_tabPanel.Children[SelectedIndex].IsEnabled) { if (_tabPanel.Children.OfType().All(x => !x.IsEnabled)) { SelectedIndex = -1; return; } TabControl_PreviewMouseWheel(sender, e); } TabItem_PreviewMouseDown(sender, null); ChangeVisibility(); } private void TabItem_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (sender is TabItem selected) selected.IsSelected = true; if (Math.Abs(_border.ActualHeight - 100) < 0) return; var animation = new DoubleAnimation(_border.ActualHeight, 100, new Duration(new TimeSpan(0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _border.BeginAnimation(HeightProperty, animation); var opacityAnimation = new DoubleAnimation(_border.Opacity, 1, new Duration(new TimeSpan(0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _border.BeginAnimation(OpacityProperty, opacityAnimation); var visibilityAnimation = new ObjectAnimationUsingKeyFrames(); visibilityAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame(Visibility.Visible, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.5)))); _hideButton.BeginAnimation(VisibilityProperty, visibilityAnimation); //Margin = 5,5,0,-1 var marginAnimation = new ThicknessAnimation(_tabPanel.Margin, new Thickness(5, 5, 0, -1), new Duration(new TimeSpan(0, 0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _tabPanel.BeginAnimation(MarginProperty, marginAnimation); } private void HideButton_Clicked(object sender, RoutedEventArgs routedEventArgs) { //ActualHeight = 0 var animation = new DoubleAnimation(_border.ActualHeight, 0, new Duration(new TimeSpan(0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _border.BeginAnimation(HeightProperty, animation); //Opacity = 0 var opacityAnimation = new DoubleAnimation(_border.Opacity, 0, new Duration(new TimeSpan(0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _border.BeginAnimation(OpacityProperty, opacityAnimation); //SelectedItem = null var objectAnimation = new ObjectAnimationUsingKeyFrames(); objectAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame(null, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)))); BeginAnimation(SelectedItemProperty, objectAnimation); //Visibility = Visibility.Collapsed var visibilityAnimation = new ObjectAnimationUsingKeyFrames(); visibilityAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame(Visibility.Collapsed, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)))); _hideButton.BeginAnimation(VisibilityProperty, visibilityAnimation); //Margin = 5,5,0,5 var marginAnimation = new ThicknessAnimation(_tabPanel.Margin, new Thickness(5, 5, 0, 5), new Duration(new TimeSpan(0, 0, 0, 0, 1))) { EasingFunction = new PowerEase { Power = 8 } }; _tabPanel.BeginAnimation(MarginProperty, marginAnimation); } private void NotificationButton_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded) return; if (_notificationButton.FindResource("NotificationStoryboard") is Storyboard story) story.Stop(); } #endregion /// /// Changes the visibility of the Content. /// /// True to show the Content. public void ChangeVisibility(bool visible = true) { _border.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; _hideButton.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; } public void UpdateVisual(bool isActivated = true) { //Shows only a white foreground when: //var color = Glass.GlassColor; //var ness = Glass.GlassColor.GetBrightness(); //var aa = color.ConvertRgbToHsv(); //var darkForeground = !SystemParameters.IsGlassEnabled || !Other.IsGlassSupported() || Glass.GlassColor.GetBrightness() > 973 || !isActivated; var darkForeground = !SystemParameters.IsGlassEnabled || !isActivated; //var darkForeground = !SystemParameters.IsGlassEnabled || !Other.IsWin8OrHigher() || aa.V > 0.5 || !isActivated; var showBackground = true;// !Other.IsGlassSupported(); //Console.WriteLine("!IsGlassEnabled: " + !SystemParameters.IsGlassEnabled); //Console.WriteLine("!UsesColor: " + !Glass.UsesColor); //Console.WriteLine("GlassColorBrightness <= 137: " + (Glass.GlassColor.GetBrightness() <= 137)); //Console.WriteLine("!IsWin8: " + !Other.IsWin8OrHigher()); //Console.WriteLine("IsActivated: " + isActivated); //Console.WriteLine("IsDark: " + isDark); //Update each tab. if (_tabPanel != null) foreach (var tab in _tabPanel.Children.OfType()) { //To force the change. if (tab.IsDark == !darkForeground) tab.IsDark = !tab.IsDark; if (tab.ShowBackground == showBackground) tab.ShowBackground = !tab.ShowBackground; tab.IsDark = !darkForeground; tab.ShowBackground = showBackground; } //Update the buttons. if (_notificationButton != null) { _notificationButton.DarkMode = !darkForeground; _notificationButton.IsOverNonClientArea = UserSettings.All.EditorExtendChrome; } if (_extrasMenuItem != null) { _extrasMenuItem.DarkMode = !darkForeground; _extrasMenuItem.IsOverNonClientArea = UserSettings.All.EditorExtendChrome; } } public void UpdateNotifications(int? id = null) { _notificationBox?.UpdateNotification(id); AnimateOrNot(); } public EncoderListViewItem AddEncoding(int id, bool isActive = false) { //Display the popup (if the editor is active) and animate the button. if (isActive) _notificationButton.IsChecked = true; AnimateOrNot(true); return _notificationBox.AddEncoding(id); } public void UpdateEncoding(int? id = null, bool onlyStatus = false) { if (!onlyStatus) _notificationBox?.UpdateEncoding(id); AnimateOrNot(); } public EncoderListViewItem RemoveEncoding(int id) { try { return _notificationBox.RemoveEncoding(id); } finally { AnimateOrNot(); } } private void AnimateOrNot(bool add = false) { var story = _notificationButton.FindResource("NotificationStoryboard") as Storyboard; if (story != null) { story.Stop(); //Blink the button when an encoding is added. if (add) story.Begin(); } var anyProcessing = EncodingManager.Encodings.Any(s => s.Status == EncodingStatus.Processing); var anyCompleted = EncodingManager.Encodings.Any(s => s.Status == EncodingStatus.Completed); var anyFaulty = EncodingManager.Encodings.Any(s => s.Status == EncodingStatus.Error); _notificationButton.Icon = anyProcessing ? FindResource("Vector.Progress") as Brush : anyCompleted ? FindResource("Vector.Ok.Round") as Brush : anyFaulty ? FindResource("Vector.Cancel.Round") as Brush : _notificationButton.Icon; _notificationButton.IsImportant = anyProcessing; _notificationButton.SetResourceReference(ExtendedToggleButton.TextProperty, anyProcessing ? "S.Encoder.Encoding" : anyCompleted ? "S.Encoder.Completed" : anyFaulty? "S.Encoder.Error" : "S.Notifications"); if (anyProcessing || anyCompleted || anyFaulty) return; //Animate the button for notifications, when there are no encodings. var most = NotificationManager.Notifications.Select(s => s.Kind).OrderByDescending(a => (int)a).FirstOrDefault(); _notificationButton.Icon = TryFindResource(StatusBand.KindToString(most)) as Brush; _notificationButton.IsImportant = most != StatusType.None; _notificationButton.SetResourceReference(ExtendedToggleButton.TextProperty, "S.Notifications"); if(story != null) { story.Stop(); if (most != StatusType.None) story.Begin(); } } } ================================================ FILE: ScreenToGif/Controls/InkCanvasExtended.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Media; namespace ScreenToGif.Controls; /// /// /// InkCanvasControl class extending the InkCanvas class /// public class InkCanvasExtended : InkCanvas { /// /// Gets or set the eraser shape /// public new StylusShape EraserShapeDependency { get => (StylusShape) GetValue(EraserShapeProperty); set => SetValue(EraserShapeProperty, value); } // Using a DependencyProperty as the backing store for EraserShape. // This enables animation, styling, binding, etc... public static readonly DependencyProperty EraserShapeProperty = DependencyProperty.Register(nameof(EraserShapeDependency), typeof(StylusShape), typeof(InkCanvasExtended), new UIPropertyMetadata(new RectangleStylusShape(10, 10), OnEraserShapePropertyChanged)); public InkCanvasExtended() { SetEnabledGestures([ApplicationGesture.NoGesture]); } /// /// Event to handle the property change /// /// dependency object /// event args private static void OnEraserShapePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not InkCanvasExtended canvas) return; canvas.EraserShape = (StylusShape) e.NewValue; canvas.RenderTransform = new MatrixTransform(); } } ================================================ FILE: ScreenToGif/Controls/IntegerBox.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; public class IntegerBox : ExtendedTextBox { private static bool _ignore; /// /// To avoid losing decimals. /// public bool UseTemporary; public double Temporary; /// /// True if it's necessary to prevent the value changed event from firing. /// public bool IgnoreValueChanged { get; set; } #region Dependency Property public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(int.MaxValue, OnMaximumPropertyChanged)); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(0, OnValuePropertyChanged)); public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(0, OnMinimumPropertyChanged)); public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(StepValue), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(1)); public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register(nameof(Offset), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(0, OnOffsetPropertyChanged)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(double), typeof(IntegerBox), new PropertyMetadata(1d, OnScalePropertyChanged)); public static readonly DependencyProperty UpdateOnInputProperty = DependencyProperty.Register(nameof(UpdateOnInput), typeof(bool), typeof(IntegerBox), new FrameworkPropertyMetadata(false, OnUpdateOnInputPropertyChanged)); public static readonly DependencyProperty DefaultValueIfEmptyProperty = DependencyProperty.Register(nameof(DefaultValueIfEmpty), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(0)); public static readonly DependencyProperty EmptyIfValueEmptyProperty = DependencyProperty.Register(nameof(EmptyIfValue), typeof(int), typeof(IntegerBox), new FrameworkPropertyMetadata(int.MinValue)); public static readonly DependencyProperty PropagateWheelEventProperty = DependencyProperty.Register(nameof(PropagateWheelEvent), typeof(bool), typeof(IntegerBox), new PropertyMetadata(default(bool))); #endregion #region Property Accessor [Bindable(true), Category("Common")] public int Maximum { get => (int)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } [Bindable(true), Category("Common")] public int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } [Bindable(true), Category("Common")] public int Minimum { get => (int)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } /// /// The Increment/Decrement value. /// [Description("The Increment/Decrement value.")] public int StepValue { get => (int)GetValue(StepProperty); set => SetValue(StepProperty, value); } [Bindable(true), Category("Common")] public int Offset { get => (int)GetValue(OffsetProperty); set => SetValue(OffsetProperty, value); } [Bindable(true), Category("Common")] public double Scale { get => (double)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } [Bindable(true), Category("Common")] public bool UpdateOnInput { get => (bool)GetValue(UpdateOnInputProperty); set => SetValue(UpdateOnInputProperty, value); } [Bindable(true), Category("Common")] public int DefaultValueIfEmpty { get => (int)GetValue(DefaultValueIfEmptyProperty); set => SetValue(DefaultValueIfEmptyProperty, value); } [Bindable(true), Category("Common")] public int EmptyIfValue { get => (int)GetValue(EmptyIfValueEmptyProperty); set => SetValue(EmptyIfValueEmptyProperty, value); } /// /// True if the wheel events should not be set as handled. /// [Bindable(true), Category("Behavior")] public bool PropagateWheelEvent { get => (bool)GetValue(PropagateWheelEventProperty); set => SetValue(PropagateWheelEventProperty, value); } #endregion #region Properties Changed private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var intBox = d as IntegerBox; if (intBox?.Value + intBox?.Offset > intBox?.Maximum) intBox.Value = intBox.Maximum + intBox.Offset; } private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is IntegerBox box) || _ignore) return; _ignore = true; if (box.Value + box.Offset > box.Maximum) { box.UseTemporary = false; box.Temporary = (box.Maximum / box.Scale) + box.Offset; box.Value = box.Maximum + box.Offset; } if (box.Value + box.Offset < box.Minimum) { box.UseTemporary = false; box.Temporary = (box.Minimum / box.Scale) + box.Offset; box.Value = box.Minimum + box.Offset; } _ignore = false; var value = ((int)Math.Round(((box.UseTemporary ? box.Temporary : box.Value) - box.Offset) * box.Scale, MidpointRounding.ToEven)); var stringValue = value == box.EmptyIfValue ? "" : value.ToString(); if (!string.Equals(box.Text, stringValue)) box.Text = stringValue; if (!box.IgnoreValueChanged) box.RaiseValueChangedEvent(); } private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var intBox = d as IntegerBox; if (intBox?.Value + intBox?.Offset < intBox?.Minimum) intBox.Value = intBox.Minimum + intBox.Offset; } private static void OnUpdateOnInputPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((IntegerBox)d).UpdateOnInput = (bool)e.NewValue; } private static void OnOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is IntegerBox box)) return; //The offset value dictates the value being displayed. //For example, The value 600 and the Offset 20 should display the text 580. //Text = Value - Offset. var value = ((int)Math.Round((box.Value - box.Offset) * box.Scale)); box.Text = value == box.EmptyIfValue ? "" : value.ToString(); } private static void OnScalePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is IntegerBox box)) return; //The scale value dictates the value being displayed. //For example, The value 600 and the scale 1.25 should display the text 750. //Text = Value * Scale. var value = ((int)Math.Round((box.Value - box.Offset) * box.Scale)); box.Text = value == box.EmptyIfValue ? "" : value.ToString(); } #endregion static IntegerBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(IntegerBox), new FrameworkPropertyMetadata(typeof(IntegerBox))); } #region Custom Events /// /// Create a custom routed event by first registering a RoutedEventID, this event uses the bubbling routing strategy. /// public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(IntegerBox)); /// /// Event raised when the numeric value is changed. /// public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } public void RaiseValueChangedEvent() { if (ValueChangedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(ValueChangedEvent); RaiseEvent(newEventArgs); } #endregion #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); var value = ((int)((Value - Offset) * Scale)); Text = value == EmptyIfValue ? "" : value.ToString(); } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (e.Source is IntegerBox) SelectAll(); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Only sets the focus if not clicking on the Up/Down buttons of a IntegerUpDown. if (e.OriginalSource is TextBlock || e.OriginalSource is Border) return; if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (string.IsNullOrEmpty(e.Text)) { e.Handled = true; return; } if (!IsEntryAllowed(e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnTextChanged(TextChangedEventArgs e) { if (!UpdateOnInput || string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) return; //The offset value dictates the value being displayed. //For example, The value 600 and the Offset 20 should display the text 580. //Value = (Text + Offset) * Scale. Temporary = Convert.ToInt32(Text, CultureInfo.CurrentUICulture) / Scale + Offset; Value = (int)Temporary; base.OnTextChanged(e); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (!UpdateOnInput) { if (string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) { Value = DefaultValueIfEmpty; return; } //The offset value dictates the value being displayed. //For example, The value 600 and the Offset 20 should display the text 580. //Value = Text + Offset. UseTemporary = true; Temporary = Convert.ToInt32(Text, CultureInfo.CurrentUICulture) / Scale + Offset; Value = (int)Math.Round(Temporary); UseTemporary = false; return; } //The offset value dictates the value being displayed. //For example, The value 600 and the Offset 20 should display the text 580. //Text = Value - Offset. var value =((int)((Value - Offset) * Scale)); Text = value == EmptyIfValue ? "" : value.ToString(); } protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Enter || e.Key == Key.Return) { e.Handled = true; MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } base.OnKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); if (!IsKeyboardFocusWithin) return; var step = Keyboard.Modifiers == (ModifierKeys.Shift | ModifierKeys.Control) ? 50 : Keyboard.Modifiers == ModifierKeys.Shift ? 10 : Keyboard.Modifiers == ModifierKeys.Control ? 5 : StepValue; Value = e.Delta > 0 ? Math.Min(Maximum + Offset, Value + step) : Math.Max(Minimum + Offset, Value - step); e.Handled = !PropagateWheelEvent; } #endregion #region Base Properties Changed private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!IsTextAllowed(text)) e.CancelCommand(); } else { e.CancelCommand(); } } #endregion #region Methods private bool IsEntryAllowed(string text) { //Only numbers. var regex = new Regex(@"^-|[0-9]$"); //Checks if it's a valid char based on the context. return regex.IsMatch(text); } private bool IsTextAllowed(string text) { return Minimum < 0 ? Regex.IsMatch(text, @"^[-]?(?:[0-9]{1,9})?$") : Regex.IsMatch(text, @"^(?:[0-9]{1,9})?$"); } #endregion } ================================================ FILE: ScreenToGif/Controls/IntegerUpDown.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; namespace ScreenToGif.Controls; /// /// Integer only control with up and down buttons to change the value. /// public class IntegerUpDown : IntegerBox { #region Variables private RepeatButton _upButton; private RepeatButton _downButton; #endregion static IntegerUpDown() { DefaultStyleKeyProperty.OverrideMetadata(typeof(IntegerUpDown), new FrameworkPropertyMetadata(typeof(IntegerUpDown))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _upButton = Template.FindName("UpButton", this) as RepeatButton; _downButton = Template.FindName("DownButton", this) as RepeatButton; if (_upButton != null) _upButton.Click += UpButton_Click; if (_downButton != null) _downButton.Click += DownButton_Click; } #region Event Handlers private void DownButton_Click(object sender, RoutedEventArgs e) { if (Value > Minimum) Value -= StepValue; } private void UpButton_Click(object sender, RoutedEventArgs e) { if (Value < Maximum) Value += StepValue; } #endregion } ================================================ FILE: ScreenToGif/Controls/Items/EncoderItem.cs ================================================ using System; using System.Windows; namespace ScreenToGif.Controls.Items; public class EncoderItem : GenericItem { public static readonly DependencyProperty EncoderTypeProperty = DependencyProperty.Register(nameof(EncoderType), typeof(Enum), typeof(EncoderItem), new PropertyMetadata(default(Enum))); public Enum EncoderType { get => (Enum) GetValue(EncoderTypeProperty); set => SetValue(EncoderTypeProperty, value); } } ================================================ FILE: ScreenToGif/Controls/Items/ExportItem.cs ================================================ using System.Windows; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls.Items; public class ExportItem : GenericItem { public static readonly DependencyProperty ExportTypeProperty = DependencyProperty.Register(nameof(ExportType), typeof(string), typeof(ExportItem), new PropertyMetadata(default(string))); public static readonly DependencyProperty FileTypeProperty = DependencyProperty.Register(nameof(FileType), typeof(ExportFormats), typeof(ExportItem), new PropertyMetadata(default(ExportFormats))); public string ExportType { get => (string) GetValue(ExportTypeProperty); set => SetValue(ExportTypeProperty, value); } public ExportFormats FileType { get => (ExportFormats) GetValue(FileTypeProperty); set => SetValue(FileTypeProperty, value); } } ================================================ FILE: ScreenToGif/Controls/Items/GenericItem.cs ================================================ using System.Windows; namespace ScreenToGif.Controls.Items; public class GenericItem : FrameworkElement { public static readonly DependencyProperty ImageIdProperty = DependencyProperty.Register(nameof(ImageId), typeof(string), typeof(GenericItem), new PropertyMetadata(default(string))); public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(GenericItem), new PropertyMetadata(default(string))); public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(GenericItem), new PropertyMetadata(default(string))); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(object), typeof(GenericItem), new PropertyMetadata(default(object))); public string ImageId { get => (string)GetValue(ImageIdProperty); set => SetValue(ImageIdProperty, value); } public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } public object Value { get => (object)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } } ================================================ FILE: ScreenToGif/Controls/Items/QuantizationMethodItem.cs ================================================ using System.Windows; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls.Items; public class QuantizationMethodItem : GenericItem { public static readonly DependencyProperty QuantizationTypeProperty = DependencyProperty.Register(nameof(QuantizationType), typeof(ColorQuantizationTypes), typeof(QuantizationMethodItem), new PropertyMetadata(default(ColorQuantizationTypes))); public ColorQuantizationTypes QuantizationType { get => (ColorQuantizationTypes) GetValue(QuantizationTypeProperty); set => SetValue(QuantizationTypeProperty, value); } } ================================================ FILE: ScreenToGif/Controls/KeyBox.cs ================================================ using ScreenToGif.Util.Helpers; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using KeyEventArgs = System.Windows.Input.KeyEventArgs; namespace ScreenToGif.Controls; public class KeyBox : ContentControl { #region Variable private bool _finished; private bool _ignore; private Key _previousKey; private ModifierKeys _previousModifier; private ExtendedButton _removeButton; #endregion #region Dependency Properties public static readonly DependencyProperty ModifierKeysProperty = DependencyProperty.Register(nameof(ModifierKeys), typeof(ModifierKeys), typeof(KeyBox), new PropertyMetadata(ModifierKeys.None, Keys_PropertyChanged)); public static readonly DependencyProperty MainKeyProperty = DependencyProperty.Register(nameof(MainKey), typeof(Key?), typeof(KeyBox), new PropertyMetadata(null, Keys_PropertyChanged)); public static readonly DependencyProperty AllowAllKeysProperty = DependencyProperty.Register(nameof(AllowAllKeys), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsControlDownProperty = DependencyProperty.Register(nameof(IsControlDown), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsShiftDownProperty = DependencyProperty.Register(nameof(IsShiftDown), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsAltDownProperty = DependencyProperty.Register(nameof(IsAltDown), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsWindowsDownProperty = DependencyProperty.Register(nameof(IsWindowsDown), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(KeyBox), new PropertyMetadata("")); public static readonly DependencyProperty IsSelectionFinishedProperty = DependencyProperty.Register(nameof(IsSelectionFinished), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty CanRemoveProperty = DependencyProperty.Register(nameof(CanRemove), typeof(bool), typeof(KeyBox), new PropertyMetadata(true)); public static readonly DependencyProperty DisplayNoneProperty = DependencyProperty.Register(nameof(DisplayNone), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty OnlyModifiersProperty = DependencyProperty.Register(nameof(OnlyModifiers), typeof(bool), typeof(KeyBox), new PropertyMetadata(false)); public static readonly DependencyProperty IsSingleLetterLowerCaseProperty = DependencyProperty.Register(nameof(IsSingleLetterLowerCase), typeof(bool), typeof(KeyBox), new PropertyMetadata(false, Keys_PropertyChanged)); public static readonly RoutedEvent KeyChangedEvent = EventManager.RegisterRoutedEvent("KeyChanged", RoutingStrategy.Bubble, typeof(KeyChangedEventHandler), typeof(KeyBox)); #endregion #region Properties public ModifierKeys ModifierKeys { get => (ModifierKeys)GetValue(ModifierKeysProperty); set => SetValue(ModifierKeysProperty, value); } public Key? MainKey { get => (Key?)GetValue(MainKeyProperty); set => SetValue(MainKeyProperty, value); } public bool AllowAllKeys { get => (bool)GetValue(AllowAllKeysProperty); set => SetValue(AllowAllKeysProperty, value); } public bool IsControlDown { get => (bool)GetValue(IsControlDownProperty); set => SetValue(IsControlDownProperty, value); } public bool IsShiftDown { get => (bool)GetValue(IsShiftDownProperty); set => SetValue(IsShiftDownProperty, value); } public bool IsAltDown { get => (bool)GetValue(IsAltDownProperty); set => SetValue(IsAltDownProperty, value); } public bool IsWindowsDown { get => (bool)GetValue(IsWindowsDownProperty); set => SetValue(IsWindowsDownProperty, value); } public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } public bool IsSelectionFinished { get => (bool)GetValue(IsSelectionFinishedProperty); set => SetValue(IsSelectionFinishedProperty, value); } public bool CanRemove { get => (bool)GetValue(CanRemoveProperty); set => SetValue(CanRemoveProperty, value); } public bool DisplayNone { get => (bool)GetValue(DisplayNoneProperty); set => SetValue(DisplayNoneProperty, value); } public bool OnlyModifiers { get => (bool)GetValue(OnlyModifiersProperty); set => SetValue(OnlyModifiersProperty, value); } public bool IsSingleLetterLowerCase { get => (bool)GetValue(IsSingleLetterLowerCaseProperty); set => SetValue(IsSingleLetterLowerCaseProperty, value); } public event KeyChangedEventHandler KeyChanged { add => AddHandler(KeyChangedEvent, value); remove => RemoveHandler(KeyChangedEvent, value); } #endregion #region Events private static void Keys_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { if (o is not KeyBox box) return; if (box.OnlyModifiers && box.ModifierKeys != ModifierKeys.None) { box.Text = KeyHelper.GetSelectKeyText(box.ModifierKeys); box.IsSelectionFinished = true; return; } if (box.MainKey == null) return; box.Text = KeyHelper.GetSelectKeyText(box.MainKey ?? Key.None, box.ModifierKeys, !(box.IsSingleLetterLowerCase && box.ModifierKeys == ModifierKeys.None), !box.DisplayNone); box.IsSelectionFinished = true; } public bool RaiseKeyChangedEvent() { var changedArgs = new KeyChangedEventArgs(KeyChangedEvent, _previousModifier, _previousKey); RaiseEvent(changedArgs); return changedArgs.Cancel; } #endregion static KeyBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(KeyBox), new FrameworkPropertyMetadata(typeof(KeyBox))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _removeButton = Template.FindName("RemoveButton", this) as ExtendedButton; if (_removeButton != null) _removeButton.Click += (sender, args) => { MainKey = Key.None; ModifierKeys = ModifierKeys.None; RaiseKeyChangedEvent(); _previousKey = Key.None; _previousModifier = ModifierKeys.None; IsSelectionFinished = true; }; _previousModifier = ModifierKeys; _previousKey = MainKey ?? Key.None; } protected override void OnMouseDown(MouseButtonEventArgs e) { Focus(); Keyboard.Focus(this); e.Handled = true; base.OnMouseDown(e); } protected override void OnPreviewKeyDown(KeyEventArgs e) { //If not all keys are allowed, enter or tab keys presses moves the focus. if (!AllowAllKeys && (e.Key == Key.Enter || e.Key == Key.Tab)) { MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); return; } //Clear current values. IsSelectionFinished = false; ModifierKeys = ModifierKeys.None; MainKey = null; // Key.None; Text = ""; _finished = false; _ignore = false; //Check the modifiers. IsControlDown = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); IsAltDown = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt); IsShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); if (AllowAllKeys) IsWindowsDown = Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin); if (OnlyModifiers) { ModifierKeys = Keyboard.Modifiers; MainKey = null; _finished = true; return; } var key = e.Key != Key.System ? e.Key : e.SystemKey; //Accept or ignore new values. if (AllowAllKeys) { //More than one modifier key without any other key. Invalid combination. if (new[] { IsControlDown, IsAltDown, IsShiftDown, IsWindowsDown }.Count(x => x) > 1 && (((int)key >= 116 && (int)key <= 121) || (int)key == 70 || (int)key == 71)) _ignore = true; else if (key > 0 && (int)key < 172) { //Cancel to OemClear. ModifierKeys = Keyboard.Modifiers; MainKey = key; //TODO: if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && (key == Key.LeftCtrl || key == Key.RightCtrl)) { IsControlDown = false; ModifierKeys = ModifierKeys.None; } else if ((Keyboard.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt && (key == Key.LeftAlt || key == Key.RightAlt)) { IsAltDown = false; ModifierKeys = ModifierKeys.None; } else if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift && (key == Key.LeftShift || key == Key.RightShift)) { IsShiftDown = false; ModifierKeys = ModifierKeys.None; } else if ((Keyboard.Modifiers & ModifierKeys.Windows) == ModifierKeys.Windows && (key == Key.LWin || key == Key.RWin)) { IsWindowsDown = false; ModifierKeys = ModifierKeys.None; } _finished = true; } } else { //If any modifier. if (IsControlDown || IsAltDown || IsShiftDown || IsWindowsDown) { if (((int)key > 33 && (int)key < 114) || ((int)key >139 && (int)key < 155)) { //D0 to F24 and Oem1 to OemBackslash. Valid combinations. ModifierKeys = Keyboard.Modifiers; MainKey = key; _finished = true; } else { //Anything else. Invalid combinations. _ignore = true; } } else { if ((int)key > 89 && (int)key < 114) { //F1 to F24. Valid single keys. ModifierKeys = Keyboard.Modifiers; MainKey = key; _finished = true; } else { //Anything else. Invalid single keys. _ignore = true; } } } e.Handled = true; base.OnPreviewKeyDown(e); } protected override void OnPreviewKeyUp(KeyEventArgs e) { if (e.Key is Key.Enter or Key.Tab && !AllowAllKeys) return; if (e.Key == Key.PrintScreen && !OnlyModifiers) { ModifierKeys = Keyboard.Modifiers; MainKey = e.Key; _finished = true; } if (_finished) { //If the values are not accepted. if (RaiseKeyChangedEvent()) { IsControlDown = ModifierKeys.HasFlag(ModifierKeys.Control); IsAltDown = ModifierKeys.HasFlag(ModifierKeys.Alt); IsShiftDown = ModifierKeys.HasFlag(ModifierKeys.Shift); if (AllowAllKeys) IsWindowsDown = ModifierKeys.HasFlag(ModifierKeys.Windows); return; } _previousKey = MainKey ?? Key.None; _previousModifier = ModifierKeys; IsSelectionFinished = true; return; } //If a invalid key combination is set, return to previous value. if (_ignore) { MainKey = _previousKey; ModifierKeys = _previousModifier; IsControlDown = ModifierKeys.HasFlag(ModifierKeys.Control); IsAltDown = ModifierKeys.HasFlag(ModifierKeys.Alt); IsShiftDown = ModifierKeys.HasFlag(ModifierKeys.Shift); IsWindowsDown = ModifierKeys.HasFlag(ModifierKeys.Windows); return; } IsControlDown = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); IsAltDown = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt); IsShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); if (AllowAllKeys) IsWindowsDown = Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin); MainKey = null; base.OnPreviewKeyUp(e); } #endregion } public delegate void KeyChangedEventHandler(object sender, KeyChangedEventArgs e); public class KeyChangedEventArgs : RoutedEventArgs { public bool Cancel { get; set; } public ModifierKeys PreviousModifiers { get; set; } public Key PreviousKey { get; set; } public ModifierKeys CurrentModifiers { get; set; } public Key CurrentKey { get; set; } public KeyChangedEventArgs(RoutedEvent routedEvent, ModifierKeys previousMod, Key previousKey, ModifierKeys currentMod, Key currentKey) { RoutedEvent = routedEvent; PreviousModifiers = previousMod; PreviousKey = previousKey; CurrentModifiers = currentMod; CurrentKey = currentKey; } public KeyChangedEventArgs(RoutedEvent routedEvent, ModifierKeys previousMod, Key previousKey) { RoutedEvent = routedEvent; PreviousModifiers = previousMod; PreviousKey = previousKey; } } ================================================ FILE: ScreenToGif/Controls/LabelSeparator.cs ================================================ using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; public class LabelSeparator : Control { #region Dependency Properties public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(LabelSeparator), new PropertyMetadata(string.Empty)); public static readonly DependencyProperty TextRightProperty = DependencyProperty.Register("TextRight", typeof(string), typeof(LabelSeparator), new PropertyMetadata(string.Empty)); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register("TextAlignment", typeof(TextAlignment), typeof(LabelSeparator), new PropertyMetadata(TextAlignment.Left)); #endregion #region Properties public string Text { get => (string) GetValue(TextProperty); set => SetValue(TextProperty, value); } public string TextRight { get => (string)GetValue(TextRightProperty); set => SetValue(TextRightProperty, value); } public TextAlignment TextAlignment { get => (TextAlignment)GetValue(TextAlignmentProperty); set => SetValue(TextAlignmentProperty, value); } #endregion static LabelSeparator() { DefaultStyleKeyProperty.OverrideMetadata(typeof(LabelSeparator), new FrameworkPropertyMetadata(typeof(LabelSeparator))); } } ================================================ FILE: ScreenToGif/Controls/LightWindow.cs ================================================ using System; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Shapes; using ScreenToGif.Domain.Enums; using ScreenToGif.Native.External; namespace ScreenToGif.Controls; /// /// Light Window used by some recorder windows. /// public class LightWindow : BaseScreenRecorder { private HwndSource _hwndSource; public DisplayTimer DisplayTimer = null; #region Dependency Property public static readonly DependencyProperty ChildProperty = DependencyProperty.Register(nameof(Child), typeof(UIElement), typeof(LightWindow), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty MaxSizeProperty = DependencyProperty.Register(nameof(MaxSize), typeof(double), typeof(LightWindow), new FrameworkPropertyMetadata(26.0, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty MinimizeVisibilityProperty = DependencyProperty.Register(nameof(MinimizeVisibility), typeof(Visibility), typeof(LightWindow), new FrameworkPropertyMetadata(Visibility.Visible, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty IsRecordingProperty = DependencyProperty.Register(nameof(IsRecording), typeof(bool), typeof(LightWindow), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty IsThinProperty = DependencyProperty.Register(nameof(IsThin), typeof(bool), typeof(LightWindow), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty IsFollowingProperty = DependencyProperty.Register(nameof(IsFollowing), typeof(bool), typeof(LightWindow), new PropertyMetadata(false, IsFollowing_PropertyChanged)); #endregion #region Property Accessor /// /// The Image of the caption bar. /// [Bindable(true), Category("Common"), Description("The Image of the caption bar.")] public UIElement Child { get => (UIElement)GetValue(ChildProperty); set => SetCurrentValue(ChildProperty, value); } /// /// The maximum size of the image. /// [Bindable(true), Category("Common"), Description("The maximum size of the image.")] public double MaxSize { get => (double)GetValue(MaxSizeProperty); set => SetCurrentValue(MaxSizeProperty, value); } /// /// Minimize button visibility. /// [Bindable(true), Category("Common"), Description("Minimize button visibility.")] public Visibility MinimizeVisibility { get => (Visibility)GetValue(MinimizeVisibilityProperty); set => SetCurrentValue(MinimizeVisibilityProperty, value); } /// /// If in recording mode. /// [Bindable(true), Category("Common"), Description("If in recording mode.")] public bool IsRecording { get => (bool)GetValue(IsRecordingProperty); set => SetCurrentValue(IsRecordingProperty, value); } /// /// Thin mode (hides the title bar). /// [Bindable(true), Category("Common"), Description("Thin mode (hides the title bar).")] public bool IsThin { get => (bool)GetValue(IsThinProperty); set => SetCurrentValue(IsThinProperty, value); } /// /// True if the window should follow the mouse cursor. /// public bool IsFollowing { get => (bool)GetValue(IsFollowingProperty); set => SetValue(IsFollowingProperty, value); } #endregion static LightWindow() { DefaultStyleKeyProperty.OverrideMetadata(typeof(LightWindow), new FrameworkPropertyMetadata(typeof(LightWindow))); } #region Overrides public override void OnApplyTemplate() { DisplayTimer = GetTemplateChild("DisplayTimer") as DisplayTimer; if (GetTemplateChild("MinimizeButton") is ExtendedButton minimizeButton) minimizeButton.Click += MinimizeClick; if (GetTemplateChild("CloseButton") is ExtendedButton closeButton) closeButton.Click += CloseClick; if (GetTemplateChild("TopGrid") is Grid topGrid) topGrid.MouseLeftButtonDown += TopGrid_MouseLeftButtonDown; if (GetTemplateChild("MainGrid") is Grid resizeGrid) { foreach (var element in resizeGrid.Children.OfType()) { element.PreviewMouseDown += ResizeRectangle_PreviewMouseDown; element.PreviewStylusButtonDown += ResizeRectangle_PreviewStylusButtonDown; } } base.OnApplyTemplate(); } protected override void OnInitialized(EventArgs e) { SourceInitialized += Window_SourceInitialized; base.OnInitialized(e); } #endregion #region Methods private void ResizeWindow(ResizeDirection direction) { User32.SendMessage(_hwndSource.Handle, 0x112, (IntPtr)(61440 + direction), IntPtr.Zero); } internal void HideInternals() { if (GetTemplateChild("MainBorder") is Border border) border.Visibility = Visibility.Hidden; } internal void ShowInternals() { if (GetTemplateChild("MainBorder") is Border border) border.Visibility = Visibility.Visible; } internal virtual void OnFollowingChanged() { } #endregion #region Events private void Window_SourceInitialized(object sender, EventArgs e) { _hwndSource = (HwndSource)PresentationSource.FromVisual(this); } private void MinimizeClick(object sender, RoutedEventArgs e) { WindowState = WindowState.Minimized; } private void RestoreClick(object sender, RoutedEventArgs e) { if (WindowState == WindowState.Normal) { WindowState = WindowState.Maximized; if (sender is ExtendedButton button) button.Icon = FindResource("Vector.Restore") as Brush; } else { WindowState = WindowState.Normal; if (sender is ExtendedButton button) button.Icon = FindResource("Vector.Maximize") as Brush; } } private void CloseClick(object sender, RoutedEventArgs e) { Close(); } private void TopGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (Mouse.LeftButton == MouseButtonState.Pressed) DragMove(); } private void ResizeRectangle_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton != MouseButton.Left) return; if (sender is not Rectangle rectangle) return; switch (rectangle.Name) { case "TopRectangle": ResizeWindow(ResizeDirection.Top); break; case "BottomRectangle": ResizeWindow(ResizeDirection.Bottom); break; case "LeftRectangle": ResizeWindow(ResizeDirection.Left); break; case "RightRectangle": ResizeWindow(ResizeDirection.Right); break; case "TopLeftRectangle": ResizeWindow(ResizeDirection.TopLeft); break; case "TopRightRectangle": ResizeWindow(ResizeDirection.TopRight); break; case "BottomLeftRectangle": ResizeWindow(ResizeDirection.BottomLeft); break; case "BottomRightRectangle": ResizeWindow(ResizeDirection.BottomRight); break; } } private void ResizeRectangle_PreviewStylusButtonDown(object sender, StylusButtonEventArgs e) { if (sender is not Rectangle rectangle) return; switch (rectangle.Name) { case "TopRectangle": ResizeWindow(ResizeDirection.Top); break; case "BottomRectangle": ResizeWindow(ResizeDirection.Bottom); break; case "LeftRectangle": ResizeWindow(ResizeDirection.Left); break; case "RightRectangle": ResizeWindow(ResizeDirection.Right); break; case "TopLeftRectangle": ResizeWindow(ResizeDirection.TopLeft); break; case "TopRightRectangle": ResizeWindow(ResizeDirection.TopRight); break; case "BottomLeftRectangle": ResizeWindow(ResizeDirection.BottomLeft); break; case "BottomRightRectangle": ResizeWindow(ResizeDirection.BottomRight); break; } } private static void IsFollowing_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not LightWindow win) return; win.OnFollowingChanged(); } #endregion } ================================================ FILE: ScreenToGif/Controls/MoveResizeControl.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace ScreenToGif.Controls; public class MoveResizeControl : ContentControl { #region Variables /// /// Resizing adorner uses Thumbs for visual elements. /// The Thumbs have built-in mouse input handling. /// private Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _top, _bottom, _left, _right; /// /// The selection rectangle, used to drag the selection Rect elsewhere. /// private Border _border; /// /// The start point for the drag operation. /// private Point _startPoint; #endregion #region Dependency Properties public static readonly DependencyProperty CanMoveProperty = DependencyProperty.Register("CanMove", typeof(bool), typeof(MoveResizeControl), new PropertyMetadata(true)); public static readonly DependencyProperty CanResizeProperty = DependencyProperty.Register("CanResize", typeof(bool), typeof(MoveResizeControl), new PropertyMetadata(false)); public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected", typeof(Rect), typeof(MoveResizeControl), new PropertyMetadata(new Rect(-1, -1, 0, 0), Selected_PropertyChanged)); public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(double), typeof(MoveResizeControl), new PropertyMetadata(-1d, Left_PropertyChanged)); public static readonly DependencyProperty TopProperty = DependencyProperty.Register("Top", typeof(double), typeof(MoveResizeControl), new PropertyMetadata(-1d, Top_PropertyChanged)); public static readonly DependencyProperty RestrictMovementProperty = DependencyProperty.Register("RestrictMovement", typeof(bool), typeof(MoveResizeControl), new PropertyMetadata(false)); public static readonly DependencyProperty ContentScaleProperty = DependencyProperty.Register("ContentScale", typeof(double), typeof(MoveResizeControl), new PropertyMetadata(1d, ContentScale_PropertyChanged)); #endregion #region Properties public bool CanMove { get => (bool) GetValue(CanMoveProperty); set => SetValue(CanMoveProperty, value); } public bool CanResize { get => (bool)GetValue(CanResizeProperty); set => SetValue(CanResizeProperty, value); } public Rect Selected { get => (Rect)GetValue(SelectedProperty); set => SetValue(SelectedProperty, value); } public double Left { get => (double)GetValue(LeftProperty); set => SetValue(LeftProperty, value); } public double Top { get => (double)GetValue(TopProperty); set => SetValue(TopProperty, value); } public bool RestrictMovement { get => (bool)GetValue(RestrictMovementProperty); set => SetValue(RestrictMovementProperty, value); } public double ContentScale { get => (double)GetValue(ContentScaleProperty); set => SetValue(ContentScaleProperty, value); } #endregion static MoveResizeControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(MoveResizeControl), new FrameworkPropertyMetadata(typeof(MoveResizeControl))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _topLeft = Template.FindName("TopLeftThumb", this) as Thumb; _topRight = Template.FindName("TopRightThumb", this) as Thumb; _bottomLeft = Template.FindName("BottomLeftThumb", this) as Thumb; _bottomRight = Template.FindName("BottomRightThumb", this) as Thumb; _top = Template.FindName("TopThumb", this) as Thumb; _bottom = Template.FindName("BottomThumb", this) as Thumb; _left = Template.FindName("LeftThumb", this) as Thumb; _right = Template.FindName("RightThumb", this) as Thumb; _border = Template.FindName("SelectBorder", this) as Border; if (_topLeft == null || _topRight == null || _bottomLeft == null || _bottomRight == null || _top == null || _bottom == null || _left == null || _right == null || _border == null) return; //Adjust the size of the element based on the content. //if (Math.Abs(Selected.Width) < 0.001 || Math.Abs(Selected.Height) < 0.001) //{ // var control = Content as FrameworkElement; // control?.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); // control?.Arrange(new Rect(control.DesiredSize)); // if (control != null && Math.Abs(control.ActualHeight) > 0.001 && Math.Abs(control.ActualWidth) > 0.001) // Selected = new Rect(Selected.X, Selected.Y, control.ActualWidth, control.ActualHeight); //} AdjustThumbs(); //Add handlers for resizing • Corners. _topLeft.DragDelta += HandleTopLeft; _topRight.DragDelta += HandleTopRight; _bottomLeft.DragDelta += HandleBottomLeft; _bottomRight.DragDelta += HandleBottomRight; //Add handlers for resizing • Sides. _top.DragDelta += HandleTop; _bottom.DragDelta += HandleBottom; _left.DragDelta += HandleLeft; _right.DragDelta += HandleRight; //Drag to move. _border.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown; _border.MouseMove += Rectangle_MouseMove; _border.MouseLeftButtonUp += Rectangle_MouseLeftButtonUp; //Detect text updates. var textBlock = Content as TextBlock; //Size down too. if (textBlock != null) textBlock.LayoutUpdated += (sender, args) => AdjustContent(); } protected override void OnChildDesiredSizeChanged(UIElement child) { AdjustContent(); base.OnChildDesiredSizeChanged(child); } protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved) { AdjustContent(); base.OnVisualChildrenChanged(visualAdded, visualRemoved); } #endregion #region Methods private void AdjustThumbs() { if (_topLeft == null) return; //Top left. Canvas.SetLeft(_topLeft, Selected.Left - _topLeft.Width / 2d); Canvas.SetTop(_topLeft, Selected.Top - _topLeft.Height / 2d); //Top right. Canvas.SetLeft(_topRight, Selected.Right - _topRight.Width / 2d); Canvas.SetTop(_topRight, Selected.Top - _topRight.Height / 2d); //Bottom left. Canvas.SetLeft(_bottomLeft, Selected.Left - _bottomLeft.Width / 2d); Canvas.SetTop(_bottomLeft, Selected.Bottom - _bottomLeft.Height / 2d); //Bottom right. Canvas.SetLeft(_bottomRight, Selected.Right - _bottomRight.Width / 2d); Canvas.SetTop(_bottomRight, Selected.Bottom - _bottomRight.Height / 2d); //Top. Canvas.SetLeft(_top, Selected.Left + Selected.Width / 2d - _top.Width / 2d); Canvas.SetTop(_top, Selected.Top - _top.Height / 2d); //Left. Canvas.SetLeft(_left, Selected.Left - _left.Width / 2d); Canvas.SetTop(_left, Selected.Top + Selected.Height / 2d - _left.Height / 2d); //Right. Canvas.SetLeft(_right, Selected.Right - _right.Width / 2d); Canvas.SetTop(_right, Selected.Top + Selected.Height / 2d - _right.Height / 2d); //Bottom. Canvas.SetLeft(_bottom, Selected.Left + Selected.Width / 2d - _bottom.Width / 2d); Canvas.SetTop(_bottom, Selected.Bottom - _bottom.Height / 2d); } public void AdjustContent() { var control = Content as FrameworkElement; if (control == null || !IsLoaded) return; control.LayoutTransform = new ScaleTransform(ContentScale, ContentScale, 0.5, 0.5); control.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); control.Arrange(new Rect(control.DesiredSize)); if (Math.Abs(control.ActualHeight) > 0.001 && Math.Abs(control.ActualWidth) > 0.001) Selected = new Rect(Selected.X, Selected.Y, control.ActualWidth * ContentScale, control.ActualHeight * ContentScale); } #endregion #region Events private static void Selected_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as MoveResizeControl; if (control == null) return; control.Left = control.Selected.Left; control.Top = control.Selected.Top; control.AdjustThumbs(); } private static void Left_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as MoveResizeControl; if (control == null) return; control.Selected = new Rect(control.Left, control.Selected.Top, control.Selected.Width, control.Selected.Height); } private static void Top_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as MoveResizeControl; if (control == null) return; control.Selected = new Rect(control.Selected.Left, control.Top, control.Selected.Width, control.Selected.Height); } private static void ContentScale_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs d) { var control = o as MoveResizeControl; control?.AdjustContent(); } private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { _startPoint = e.GetPosition(this); _border.CaptureMouse(); e.Handled = true; } private void Rectangle_MouseMove(object sender, MouseEventArgs e) { if (!_border.IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; _border.MouseMove -= Rectangle_MouseMove; var currentPosition = e.GetPosition(this); var x = Selected.X + (currentPosition.X - _startPoint.X); var y = Selected.Y + (currentPosition.Y - _startPoint.Y); if (RestrictMovement) { if (x < 0) x = 0; if (y < 0) y = 0; if (x + Selected.Width > ActualWidth) x = ActualWidth - Selected.Width; if (y + Selected.Height > ActualHeight) y = ActualHeight - Selected.Height; } else { if (x < Selected.Width * -0.9) x = Selected.Width * -0.9; if (y < Selected.Height * -0.9) y = Selected.Height * -0.9; if (x + Selected.Width > ActualWidth + Selected.Width * 0.9) x = ActualWidth - Selected.Width + Selected.Width * 0.9; if (y + Selected.Height > ActualHeight + Selected.Height * 0.9) y = ActualHeight - Selected.Height + Selected.Height * 0.9; } Selected = new Rect(x, y, Selected.Width, Selected.Height); _startPoint = currentPosition; e.Handled = true; AdjustThumbs(); _border.MouseMove += Rectangle_MouseMove; } private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (_border.IsMouseCaptured) _border.ReleaseMouseCapture(); AdjustThumbs(); e.Handled = true; } /// ///Handler for resizing from the top-left. /// private void HandleTopLeft(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, top, width, height); AdjustThumbs(); } /// /// Handler for resizing from the top-right. /// private void HandleTopRight(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, top, width, height); AdjustThumbs(); } /// /// Handler for resizing from the bottom-left. /// private void HandleBottomLeft(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (left < 0) { width -= left * -1; left = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(left, Selected.Top, width, height); AdjustThumbs(); } /// /// Handler for resizing from the bottom-right. /// private void HandleBottomRight(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, width, height); AdjustThumbs(); } /// /// Handler for resizing from the left-middle. /// private void HandleLeft(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, Selected.Top, width, Selected.Height); AdjustThumbs(); } /// /// Handler for resizing from the top-middle. /// private void HandleTop(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } Selected = new Rect(Selected.Left, top, Selected.Width, height); AdjustThumbs(); } /// /// Handler for resizing from the right-middle. /// private void HandleRight(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, Selected.Top, width, Selected.Height); AdjustThumbs(); } /// /// Handler for resizing from the bottom-middle. /// private void HandleBottom(object sender, DragDeltaEventArgs e) { var hitThumb = sender as Thumb; if (hitThumb == null) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, Selected.Width, height); AdjustThumbs(); } #endregion } ================================================ FILE: ScreenToGif/Controls/NotificationBox.cs ================================================ using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; using ScreenToGif.Domain.Enums; using ScreenToGif.Util; namespace ScreenToGif.Controls; [DefaultProperty("Items")] [ContentProperty("Items")] public class NotificationBox : ItemsControl { #region Variables /// /// The start point of the dragging operation. /// private Point _dragStart = new Point(0, 0); private Hyperlink _notificationHyperlink; private Hyperlink _encodingHyperlink; private ScrollViewer _mainScrollViewer; #endregion #region Properties public static readonly DependencyProperty HasAnyNotificationProperty = DependencyProperty.Register(nameof(HasAnyNotification), typeof(bool), typeof(NotificationBox), new PropertyMetadata(false)); public static readonly DependencyProperty HasAnyEncodingProperty = DependencyProperty.Register(nameof(HasAnyEncoding), typeof(bool), typeof(NotificationBox), new PropertyMetadata(false)); public static readonly DependencyProperty HasAnyActiveEncodingProperty = DependencyProperty.Register(nameof(HasAnyActiveEncoding), typeof(bool), typeof(NotificationBox), new PropertyMetadata(false)); public static readonly DependencyProperty OnlyDisplayListProperty = DependencyProperty.Register(nameof(OnlyDisplayList), typeof(bool), typeof(NotificationBox), new PropertyMetadata(false)); public bool HasAnyNotification { get => (bool) GetValue(HasAnyNotificationProperty); set => SetValue(HasAnyNotificationProperty, value); } public bool HasAnyEncoding { get => (bool)GetValue(HasAnyEncodingProperty); set => SetValue(HasAnyEncodingProperty, value); } public bool HasAnyActiveEncoding { get => (bool)GetValue(HasAnyActiveEncodingProperty); set => SetValue(HasAnyActiveEncodingProperty, value); } public bool OnlyDisplayList { get => (bool)GetValue(OnlyDisplayListProperty); set => SetValue(OnlyDisplayListProperty, value); } #endregion static NotificationBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(NotificationBox), new FrameworkPropertyMetadata(typeof(NotificationBox))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _notificationHyperlink = GetTemplateChild("NotificationHyperlink") as Hyperlink; _encodingHyperlink = GetTemplateChild("EncodingHyperlink") as Hyperlink; _mainScrollViewer = GetTemplateChild("MainScrollViewer") as ScrollViewer; IsVisibleChanged += (sender, args) => { if (!IsLoaded || !IsVisible) return; CheckIfFileExist(); }; if (_notificationHyperlink != null) _notificationHyperlink.Click += NotificationHyperlink_Click; if (_encodingHyperlink != null) _encodingHyperlink.Click += EncodingHyperlink_Click; UpdateNotification(); UpdateEncoding(); } #region Events private void Encoding_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { //Don't start the drag and drop if the user clicks on some button on the encoder. if (!(sender is EncoderListViewItem item) || e.OriginalSource is Run || VisualHelper.HasParent(e.OriginalSource as Visual, typeof(EncoderListViewItem), true)) return; item.CaptureMouse(); _dragStart = e.GetPosition(null); } private void Encoding_MouseMove(object sender, MouseEventArgs e) { var diff = _dragStart - e.GetPosition(null); if (e.LeftButton != MouseButtonState.Pressed || !(Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance) || !(Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)) return; if (!(sender is EncoderListViewItem enc) || enc.Status != EncodingStatus.Completed || !File.Exists(enc.OutputFilename) || !enc.IsMouseCaptured) return; //To support multiple files in drag, use ListBox or ListView and get the selected items: //var files = ListView.SelectedItems.OfType().Where(y => y.Status == Status.Completed && File.Exists(y.OutputFilename)).Select(x => x.OutputFilename).ToArray(); DragDrop.DoDragDrop(this, new DataObject(DataFormats.FileDrop, new[] { enc.OutputFilename }), Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) ? DragDropEffects.Copy : DragDropEffects.Move); } private void Encoding_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { var item = sender as UIElement; item?.ReleaseMouseCapture(); } private void CancelEncoding_Clicked(object sender, RoutedEventArgs args) { if (!(sender is EncoderListViewItem item)) return; if (item.Status != EncodingStatus.Processing) EncodingManager.RemoveEncodings(item.Id); else if (!item.TokenSource.IsCancellationRequested) item.TokenSource.Cancel(); } private void RemoveNotification_Click(object sender, RoutedEventArgs args) { if (!(sender is StatusBand band)) return; NotificationManager.RemoveNotification(band.Id); } private void NotificationHyperlink_Click(object sender, RoutedEventArgs args) { NotificationManager.RemoveAllNotifications(); } private void EncodingHyperlink_Click(object sender, RoutedEventArgs args) { EncodingManager.RemoveFinishedEncodings(); } #endregion #region UI manipulation public void UpdateNotification(int? id = null) { var evergreen = id.HasValue ? NotificationManager.Notifications.Where(w => w.Id == id.Value).ToList() : NotificationManager.Notifications; var dirty = id.HasValue ? Items.OfType().Where(w => w.Id == id).ToList() : Items.OfType().ToList(); //Add items. var include = evergreen.Where(w => dirty.All(a => a.Id != w.Id)).ToList(); foreach (var item in include) { var not = new StatusBand { Id = item.Id, Text = item.Text, Type = item.Kind, Action = item.Action, IsLink = item.Action != null, Visibility = Visibility.Visible }; not.Dismissed += RemoveNotification_Click; Items.Add(not); } //Remove items that dont exist anymore. var remove = dirty.Where(w => evergreen.All(a => a.Id != w.Id)).ToList(); foreach (var item in remove) { item.Dismissed -= RemoveNotification_Click; Items.Remove(item); } //Update others. var update = evergreen.Where(w => dirty.Any(a => a.Id == w.Id)).ToList(); foreach (var item in update) { var actual = Items.OfType().FirstOrDefault(w => w.Id == item.Id); if (actual == null) continue; actual.Text = item.Text; //TODO: Should this exist? } CommandManager.InvalidateRequerySuggested(); GC.Collect(); HasAnyNotification = Items.OfType().Any(); } public EncoderListViewItem AddEncoding(int id) { var item = EncodingManager.Encodings.FirstOrDefault(w => w.Id == id); if (item == null) return null; //Check if the enoder item was added before, during initialization. if (Items.OfType().Any(a => a.Id == id)) return null; var enc = new EncoderListViewItem { Id = item.Id, OutputType = item.OutputType, Text = item.Text, Status = item.Status, CurrentFrame = item.CurrentFrame, FrameCount = item.FrameCount, TokenSource = item.TokenSource, //These following properties are only available if an IEncoding window is opened after an encoding was already inserted. IsIndeterminate = item.IsIndeterminate, SizeInBytes = item.SizeInBytes, OutputFilename = item.OutputFilename, SavedToDisk = item.SavedToDisk, Uploaded = item.Uploaded, UploadLink = item.UploadLink, UploadLinkDisplay = item.UploadLinkDisplay, DeletionLink = item.DeletionLink, Exception = item.Exception, CommandExecuted = item.CommandExecuted, Command = item.Command, CommandOutput = item.CommandOutput, CommandTaskException = item.CommandTaskException, CopiedToClipboard = item.CopiedToClipboard, CopyTaskException = item.CopyTaskException, TimeToAnalyze = item.TimeToAnalyze, TimeToEncode = item.TimeToEncode, TimeToUpload = item.TimeToUpload, TimeToCopy = item.TimeToCopy, TimeToExecute = item.TimeToExecute, }; enc.CancelClicked += CancelEncoding_Clicked; enc.PreviewMouseLeftButtonDown += Encoding_PreviewMouseLeftButtonDown; enc.PreviewMouseLeftButtonUp += Encoding_PreviewMouseLeftButtonUp; enc.MouseMove += Encoding_MouseMove; Items.Add(enc); _mainScrollViewer?.ScrollToBottom(); HasAnyEncoding = Items.OfType().Any(); HasAnyActiveEncoding = Items.OfType().Any(a => a.Status == EncodingStatus.Processing); return enc; } public void UpdateEncoding(int? id = null) { var evergreen = id.HasValue ? EncodingManager.Encodings.Where(w => w.Id == id.Value).ToList() : EncodingManager.Encodings; var dirty = id.HasValue ? Items.OfType().Where(w => w.Id == id).ToList() : Items.OfType().ToList(); //Add items. var include = evergreen.Where(w => dirty.All(a => a.Id != w.Id)).ToList(); foreach (var item in include) { var enc = new EncoderListViewItem { Id = item.Id, OutputType = item.OutputType, Text = item.Text, Status = item.Status, CurrentFrame = item.CurrentFrame, FrameCount = item.FrameCount, TokenSource = item.TokenSource, //These following properties are only available if an IEncoding window is opened after an encoding was already inserted. IsIndeterminate = item.IsIndeterminate, SizeInBytes = item.SizeInBytes, OutputFilename = item.OutputFilename, SavedToDisk = item.SavedToDisk, Uploaded = item.Uploaded, UploadLink = item.UploadLink, UploadLinkDisplay = item.UploadLinkDisplay, DeletionLink = item.DeletionLink, Exception = item.Exception, CommandExecuted = item.CommandExecuted, Command = item.Command, CommandOutput = item.CommandOutput, CommandTaskException = item.CommandTaskException, CopiedToClipboard = item.CopiedToClipboard, CopyTaskException = item.CopyTaskException, TimeToAnalyze = item.TimeToAnalyze, TimeToEncode = item.TimeToEncode, TimeToUpload = item.TimeToUpload, TimeToCopy = item.TimeToCopy, TimeToExecute = item.TimeToExecute, }; enc.CancelClicked += CancelEncoding_Clicked; enc.PreviewMouseLeftButtonDown += Encoding_PreviewMouseLeftButtonDown; enc.PreviewMouseLeftButtonUp += Encoding_PreviewMouseLeftButtonUp; enc.MouseMove += Encoding_MouseMove; EncodingManager.ViewList.Add(enc); Items.Add(enc); } //Remove items that dont exist anymore. var remove = dirty.Where(w => evergreen.All(a => a.Id != w.Id)).ToList(); foreach (var item in remove) { item.CancelClicked -= CancelEncoding_Clicked; item.PreviewMouseLeftButtonDown -= Encoding_PreviewMouseLeftButtonDown; item.PreviewMouseLeftButtonUp -= Encoding_PreviewMouseLeftButtonUp; item.MouseMove -= Encoding_MouseMove; EncodingManager.ViewList.Remove(item); Items.Remove(item); } //Update others. var update = evergreen.Where(w => dirty.Any(a => a.Id == w.Id)).ToList(); foreach (var item in update) { //var current = EncodingManager.Encodings.FirstOrDefault(w => w.Id == item.Id); var actual = Items.OfType().FirstOrDefault(w => w.Id == item.Id); if (actual == null) continue; actual.Text = item.Text; actual.Status = item.Status; actual.IsIndeterminate = item.IsIndeterminate; actual.CurrentFrame = item.CurrentFrame; actual.FrameCount = item.FrameCount; actual.SizeInBytes = item.SizeInBytes; actual.OutputFilename = item.OutputFilename; actual.SavedToDisk = item.SavedToDisk; actual.Exception = item.Exception; actual.Uploaded = item.Uploaded; actual.UploadLink = item.UploadLink; actual.UploadLinkDisplay = item.UploadLinkDisplay; actual.DeletionLink = item.DeletionLink; actual.CommandExecuted = item.CommandExecuted; actual.Command = item.Command; actual.CommandOutput = item.CommandOutput; actual.CommandTaskException = item.CommandTaskException; actual.CopiedToClipboard = item.CopiedToClipboard; actual.CopyTaskException = item.CopyTaskException; actual.TimeToAnalyze = item.TimeToAnalyze; actual.TimeToEncode = item.TimeToEncode; actual.TimeToUpload = item.TimeToUpload; actual.TimeToCopy = item.TimeToCopy; actual.TimeToExecute = item.TimeToExecute; } CommandManager.InvalidateRequerySuggested(); GC.Collect(); HasAnyEncoding = Items.OfType().Any(); HasAnyActiveEncoding = Items.OfType().Any(a => a.Status == EncodingStatus.Processing); } public EncoderListViewItem RemoveEncoding(int id) { //Removes encoding. var item = EncodingManager.Encodings.FirstOrDefault(w => w.Id == id); if (item != null) Items.Remove(item); //Removes view. var enc = Items.OfType().FirstOrDefault(w => w.Id == id); if (enc == null) return null; enc.CancelClicked -= CancelEncoding_Clicked; enc.PreviewMouseLeftButtonDown -= Encoding_PreviewMouseLeftButtonDown; enc.PreviewMouseLeftButtonUp -= Encoding_PreviewMouseLeftButtonUp; enc.MouseMove -= Encoding_MouseMove; EncodingManager.ViewList.Remove(enc); Items.Remove(enc); HasAnyEncoding = Items.OfType().Any(); HasAnyActiveEncoding = Items.OfType().Any(a => a.Status == EncodingStatus.Processing); return enc; } public void CheckIfFileExist() { foreach (var item in EncodingManager.Encodings.Where(item => item.Status == EncodingStatus.Completed || item.Status == EncodingStatus.FileDeletedOrMoved)) { if (!File.Exists(item.OutputFilename) && !item.AreMultipleFiles) EncodingManager.Update(item.Id, EncodingStatus.FileDeletedOrMoved); else if (item.Status == EncodingStatus.FileDeletedOrMoved) EncodingManager.Update(item.Id, EncodingStatus.Completed, item.OutputFilename); } } /// /// Removes all views from this instance. /// This method is used when the encoder window closes and needs to remove the references from the manager. /// public void RemoveAllViews() { var list = Items.OfType().ToList(); foreach (var enc in list) { enc.CancelClicked -= CancelEncoding_Clicked; enc.PreviewMouseLeftButtonDown -= Encoding_PreviewMouseLeftButtonDown; enc.MouseMove -= Encoding_MouseMove; EncodingManager.ViewList.Remove(enc); Items.Remove(enc); } } #endregion } ================================================ FILE: ScreenToGif/Controls/NotifyIcon.cs ================================================ using System; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Enums.Native; using ScreenToGif.ImageUtil; using ScreenToGif.Native.External; using ScreenToGif.Native.Helpers; using ScreenToGif.Native.Structs; using ScreenToGif.Util; using Other = ScreenToGif.Util.Other; namespace ScreenToGif.Controls; internal class NotifyIcon : FrameworkElement, IDisposable { #region Variables private Icon _icon; /// /// Represents the current icon data. /// private NotifyIconData _iconData; /// /// Receives messages from the taskbar icon. /// private readonly WindowMessageSink _messageSink; #endregion #region Dependencies public static readonly DependencyProperty IconSourceProperty = DependencyProperty.Register(nameof(IconSource), typeof(ImageSource), typeof(NotifyIcon), new FrameworkPropertyMetadata(null, IconSourcePropertyChanged)); public static readonly DependencyProperty NotifyToolTipProperty = DependencyProperty.Register(nameof(NotifyToolTip), typeof(UIElement), typeof(NotifyIcon), new FrameworkPropertyMetadata(null, ToolTipPropertyChanged)); public static readonly DependencyProperty NotifyToolTipTextProperty = DependencyProperty.Register(nameof(NotifyToolTipText), typeof(string), typeof(NotifyIcon), new FrameworkPropertyMetadata(string.Empty, ToolTipTextPropertyChanged)); private static readonly DependencyPropertyKey NotifyToolTipElementPropertyKey = DependencyProperty.RegisterReadOnly(nameof(NotifyToolTipElement), typeof(ToolTip), typeof(NotifyIcon), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty NotifyToolTipElementProperty = NotifyToolTipElementPropertyKey.DependencyProperty; private static readonly DependencyProperty LeftClickCommandProperty = DependencyProperty.Register(nameof(LeftClickCommand), typeof(ICommand), typeof(NotifyIcon), new FrameworkPropertyMetadata(null)); private static readonly DependencyProperty LeftDoubleClickCommandProperty = DependencyProperty.Register(nameof(LeftDoubleClickCommand), typeof(ICommand), typeof(NotifyIcon), new FrameworkPropertyMetadata(null)); private static readonly DependencyProperty MiddleClickCommandProperty = DependencyProperty.Register(nameof(MiddleClickCommand), typeof(ICommand), typeof(NotifyIcon), new FrameworkPropertyMetadata(null)); public static readonly RoutedEvent TrayMouseMoveEvent = EventManager.RegisterRoutedEvent(nameof(TrayMouseMove), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayLeftMouseDownEvent = EventManager.RegisterRoutedEvent(nameof(TrayLeftMouseDown), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayRightMouseDownEvent = EventManager.RegisterRoutedEvent(nameof(TrayRightMouseDown), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayMiddleMouseDownEvent = EventManager.RegisterRoutedEvent(nameof(TrayMiddleMouseDown), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayLeftMouseUpEvent = EventManager.RegisterRoutedEvent(nameof(TrayLeftMouseUp), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayRightMouseUpEvent = EventManager.RegisterRoutedEvent(nameof(TrayRightMouseUp), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayMiddleMouseUpEvent = EventManager.RegisterRoutedEvent(nameof(TrayMiddleMouseUp), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayMouseDoubleClickEvent = EventManager.RegisterRoutedEvent(nameof(TrayMouseDoubleClick), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent PreviewTrayContextMenuOpenEvent = EventManager.RegisterRoutedEvent(nameof(PreviewTrayContextMenuOpen), RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent TrayContextMenuOpenEvent = EventManager.RegisterRoutedEvent(nameof(TrayContextMenuOpen), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent PreviewToolTipOpenEvent = EventManager.RegisterRoutedEvent(nameof(PreviewToolTipOpen), RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent ToolTipOpenEvent = EventManager.RegisterRoutedEvent(nameof(ToolTipOpen), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent PreviewToolTipCloseEvent = EventManager.RegisterRoutedEvent(nameof(PreviewToolTipClose), RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(NotifyIcon)); public static readonly RoutedEvent ToolTipCloseEvent = EventManager.RegisterRoutedEvent(nameof(ToolTipClose), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NotifyIcon)); #endregion #region Properties /// /// Indicates whether the taskbar icon has been created or not. /// public bool IsTaskbarIconCreated { get; private set; } /// /// Checks whether a non-tooltip popup is currently opened. /// private bool IsPopupOpen => ContextMenu?.IsOpen ?? false; public bool IsDisposed { get; private set; } [Browsable(false)] public Icon Icon { get => _icon; set { _icon = value; _iconData.IconHandle = value == null ? IntPtr.Zero : (_icon?.Handle ?? IntPtr.Zero); NotifyIconHelper.WriteIconData(ref _iconData, NotifyCommands.Modify, IconDataMembers.Icon); } } public ImageSource IconSource { get => (ImageSource)GetValue(IconSourceProperty); set => SetValue(IconSourceProperty, value); } public string NotifyToolTipText { get => (string)GetValue(NotifyToolTipTextProperty); set => SetValue(NotifyToolTipTextProperty, value); } public UIElement? NotifyToolTip { get => (UIElement)GetValue(NotifyToolTipProperty); set => SetValue(NotifyToolTipProperty, value); } [Bindable(true)] public ToolTip? NotifyToolTipElement => (ToolTip)GetValue(NotifyToolTipElementProperty); public ICommand LeftClickCommand { get => (ICommand)GetValue(LeftClickCommandProperty); set => SetValue(LeftClickCommandProperty, value); } public ICommand LeftDoubleClickCommand { get => (ICommand)GetValue(LeftDoubleClickCommandProperty); set => SetValue(LeftDoubleClickCommandProperty, value); } public ICommand MiddleClickCommand { get => (ICommand)GetValue(MiddleClickCommandProperty); set => SetValue(MiddleClickCommandProperty, value); } public event RoutedEventHandler TrayMouseMove { add => AddHandler(TrayMouseMoveEvent, value); remove => RemoveHandler(TrayMouseMoveEvent, value); } public event RoutedEventHandler TrayLeftMouseDown { add => AddHandler(TrayLeftMouseDownEvent, value); remove => RemoveHandler(TrayLeftMouseDownEvent, value); } public event RoutedEventHandler TrayRightMouseDown { add => AddHandler(TrayRightMouseDownEvent, value); remove => RemoveHandler(TrayRightMouseDownEvent, value); } public event RoutedEventHandler TrayMiddleMouseDown { add => AddHandler(TrayMiddleMouseDownEvent, value); remove => RemoveHandler(TrayMiddleMouseDownEvent, value); } public event RoutedEventHandler TrayLeftMouseUp { add => AddHandler(TrayLeftMouseUpEvent, value); remove => RemoveHandler(TrayLeftMouseUpEvent, value); } public event RoutedEventHandler TrayRightMouseUp { add => AddHandler(TrayRightMouseUpEvent, value); remove => RemoveHandler(TrayRightMouseUpEvent, value); } public event RoutedEventHandler TrayMiddleMouseUp { add => AddHandler(TrayMiddleMouseUpEvent, value); remove => RemoveHandler(TrayMiddleMouseUpEvent, value); } public event RoutedEventHandler TrayMouseDoubleClick { add => AddHandler(TrayMouseDoubleClickEvent, value); remove => RemoveHandler(TrayMouseDoubleClickEvent, value); } public event RoutedEventHandler PreviewTrayContextMenuOpen { add => AddHandler(PreviewTrayContextMenuOpenEvent, value); remove => RemoveHandler(PreviewTrayContextMenuOpenEvent, value); } public event RoutedEventHandler TrayContextMenuOpen { add => AddHandler(TrayContextMenuOpenEvent, value); remove => RemoveHandler(TrayContextMenuOpenEvent, value); } public event RoutedEventHandler PreviewToolTipOpen { add => AddHandler(PreviewToolTipOpenEvent, value); remove => RemoveHandler(PreviewToolTipOpenEvent, value); } public event RoutedEventHandler ToolTipOpen { add => AddHandler(ToolTipOpenEvent, value); remove => RemoveHandler(ToolTipOpenEvent, value); } public event RoutedEventHandler PreviewToolTipClose { add => AddHandler(PreviewToolTipCloseEvent, value); remove => RemoveHandler(PreviewToolTipCloseEvent, value); } public event RoutedEventHandler ToolTipClose { add => AddHandler(ToolTipCloseEvent, value); remove => RemoveHandler(ToolTipCloseEvent, value); } #endregion #region Property Changes private static void VisibilityPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as NotifyIcon; var newValue = (Visibility)e.NewValue; if (control == null) return; if (newValue == Visibility.Visible) control.CreateTaskbarIcon(); else control.RemoveTaskbarIcon(); } private static void DataContextPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { if (o is not NotifyIcon control) return; control.UpdateDataContext(control.NotifyToolTipElement, e.OldValue, e.NewValue); control.UpdateDataContext(control.ContextMenu, e.OldValue, e.NewValue); } private static void ContextMenuPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as NotifyIcon; if (e.NewValue is not ContextMenu newValue) return; control?.UpdateDataContext(newValue, null, control.DataContext); } private static void IconSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var owner = d as NotifyIcon; var value = (ImageSource)e.NewValue; if (owner != null && value != null && !VisualHelper.IsInDesignMode()) owner.Icon = value.ToIcon(); } private static void ToolTipPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not NotifyIcon owner) return; //owner.CreateCustomToolTip(); owner.WriteToolTipSettings(); } private static void ToolTipTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not NotifyIcon owner) return; //if (owner.NotifyToolTip == null) //{ // //Create or just update the tooltip. // if (owner.NotifyToolTipElement == null) // owner.CreateCustomToolTip(); // else // owner.NotifyToolTipElement.Content = e.NewValue; //} owner.WriteToolTipSettings(); } #endregion static NotifyIcon() { VisibilityProperty.OverrideMetadata(typeof(NotifyIcon), new PropertyMetadata(Visibility.Visible, VisibilityPropertyChanged)); DataContextProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(DataContextPropertyChanged)); ContextMenuProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(ContextMenuPropertyChanged)); } public NotifyIcon() { _messageSink = new WindowMessageSink(); _iconData = NotifyIconData.CreateDefault(_messageSink.MessageWindowHandle); _messageSink.MouseEventReceived += OnMouseEvent; _messageSink.TaskbarCreated += OnTaskbarCreated; _messageSink.ChangeToolTipStateRequest += OnToolTipChange; if (Application.Current != null) Application.Current.Exit += OnExit; } #region Methods public override void OnApplyTemplate() { base.OnApplyTemplate(); RefreshVisual(); } private void CreateTaskbarIcon() { lock (this) { if (IsTaskbarIconCreated) return; _iconData.VersionOrTimeout = (uint)NotifyIconVersions.Vista; _iconData.ValidMembers = IconDataMembers.Icon | IconDataMembers.Tip | IconDataMembers.Message; _iconData.ToolTipText = NotifyToolTipText; var status = Shell32.Shell_NotifyIcon(NotifyCommands.Add, ref _iconData); if (!status) return; IsTaskbarIconCreated = true; } } private void RemoveTaskbarIcon() { lock (this) { if (!IsTaskbarIconCreated) return; NotifyIconHelper.WriteIconData(ref _iconData, NotifyCommands.Delete, IconDataMembers.Message); IsTaskbarIconCreated = false; } } public PointW GetDeviceCoordinates(PointW point) { var dpi = Other.ScaleOfSystem(); return new PointW { X = (int)(point.X / dpi), Y = (int)(point.Y / dpi) }; } private void ShowContextMenu(PointW cursorPosition) { if (IsDisposed) return; var args = new RoutedEventArgs { RoutedEvent = PreviewTrayContextMenuOpenEvent }; RaiseEvent(args); if (args.Handled || ContextMenu == null) return; ContextMenu.Placement = PlacementMode.AbsolutePoint; ContextMenu.HorizontalOffset = cursorPosition.X; ContextMenu.VerticalOffset = cursorPosition.Y; ContextMenu.IsOpen = true; //Gets the handle from the context menu or from the message sink. var handle = ((HwndSource)PresentationSource.FromVisual(ContextMenu))?.Handle ?? _messageSink.MessageWindowHandle; //This makes sure that the context menu can close if lost focus. User32.SetForegroundWindow(handle); RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayContextMenuOpenEvent }); } private void UpdateDataContext(FrameworkElement target, object oldDataContextValue, object newDataContextValue) { if (target == null || target.IsDataContextDataBound()) return; //if the target's data context is the NotifyIcon's old DataContext or the NotifyIcon itself, update it. if (ReferenceEquals(this, target.DataContext) || Equals(oldDataContextValue, target.DataContext)) target.DataContext = newDataContextValue ?? this; } private void CreateCustomToolTip() { var tt = NotifyToolTip as ToolTip; if (tt == null && NotifyToolTip != null) { tt = new ToolTip { Placement = PlacementMode.Mouse, HasDropShadow = false, BorderThickness = new Thickness(0), Background = System.Windows.Media.Brushes.Transparent, StaysOpen = true, Content = NotifyToolTip }; } else if (tt == null && !string.IsNullOrEmpty(NotifyToolTipText)) { tt = new ToolTip { Content = NotifyToolTipText }; } if (tt != null) UpdateDataContext(tt, null, DataContext); //Store a reference to the used tooltip. //SetValue(NotifyToolTipElementPropertyKey, tt); } private void WriteToolTipSettings() { lock (this) { _iconData.ToolTipText = NotifyToolTipText; Shell32.Shell_NotifyIcon(NotifyCommands.Modify, ref _iconData); } } public void RefreshVisual() { if (ContextMenu == null) return; foreach (var menuItem in ContextMenu.Items.OfType()) { menuItem.Foreground = TryFindResource("Element.Foreground.Medium") as SolidColorBrush; if (menuItem.Name == "ExitButton") menuItem.Icon = TryFindResource("Vector.Close") as System.Windows.Media.Brush; } if (NotifyToolTipElement != null) { //For some reason, the context menu of the systray icon is not updating its style. NotifyToolTipElement.Background = ContextMenu.Background = TryFindResource("Element.Background") as SolidColorBrush; NotifyToolTipElement.SetValue(TextBlock.ForegroundProperty, TryFindResource("Element.Foreground.Medium") as SolidColorBrush); NotifyToolTipElement.InvalidateVisual(); } } #endregion #region Events protected override void OnInitialized(EventArgs e) { if (Visibility == Visibility.Visible) CreateTaskbarIcon(); base.OnInitialized(e); } private void OnMouseEvent(MouseEventType type) { if (IsDisposed) return; switch (type) { case MouseEventType.MouseMove: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayMouseMoveEvent }); return; case MouseEventType.IconLeftMouseDown: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayLeftMouseDownEvent }); break; case MouseEventType.IconRightMouseDown: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayRightMouseDownEvent }); break; case MouseEventType.IconMiddleMouseDown: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayMiddleMouseDownEvent }); break; case MouseEventType.IconLeftMouseUp: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayLeftMouseUpEvent }); LeftClickCommand?.Execute(this); break; case MouseEventType.IconRightMouseUp: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayRightMouseUpEvent }); break; case MouseEventType.IconMiddleMouseUp: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayMiddleMouseUpEvent }); MiddleClickCommand?.Execute(this); break; case MouseEventType.IconLeftDoubleClick: RaiseEvent(new RoutedEventArgs { RoutedEvent = TrayMouseDoubleClickEvent }); LeftDoubleClickCommand?.Execute(this); break; default: throw new ArgumentOutOfRangeException(nameof(type), "Missing handler for mouse event flag: " + type); } var cursorPosition = new PointW(); User32.GetPhysicalCursorPos(ref cursorPosition); cursorPosition = GetDeviceCoordinates(cursorPosition); if (type == MouseEventType.IconRightMouseUp) ShowContextMenu(cursorPosition); } private void OnTaskbarCreated() { IsTaskbarIconCreated = false; CreateTaskbarIcon(); } private void OnToolTipChange(bool visible) { if (NotifyToolTipElement == null) return; if (visible) { if (IsPopupOpen) return; var args = new RoutedEventArgs { RoutedEvent = PreviewToolTipOpenEvent }; RaiseEvent(args); if (args.Handled) return; try { NotifyToolTipElement.IsOpen = true; NotifyToolTip?.RaiseEvent(new RoutedEventArgs { RoutedEvent = ToolTipOpenEvent }); RaiseEvent(new RoutedEventArgs { RoutedEvent = ToolTipOpenEvent }); } catch (Exception e) { LogWriter.Log(e, "Trying to open system tray popup"); } } else { var args = new RoutedEventArgs { RoutedEvent = PreviewToolTipCloseEvent }; RaiseEvent(args); if (args.Handled) return; NotifyToolTip?.RaiseEvent(new RoutedEventArgs { RoutedEvent = ToolTipCloseEvent }); NotifyToolTipElement.IsOpen = false; RaiseEvent(new RoutedEventArgs { RoutedEvent = ToolTipCloseEvent }); } } private void OnExit(object sender, EventArgs e) { Dispose(); } #endregion #region Disposing public void Dispose() { Dispose(true); //Avoid disposing twice. GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (IsDisposed || !disposing) return; lock (this) { IsDisposed = true; if (Application.Current != null) Application.Current.Exit -= OnExit; _messageSink.Dispose(); RemoveTaskbarIcon(); } } #endregion } ================================================ FILE: ScreenToGif/Controls/NullableIntegerBox.cs ================================================ using System; using System.ComponentModel; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ScreenToGif.Controls; public class NullableIntegerBox : ExtendedTextBox { private static bool _ignore; /// /// To avoid losing decimals. /// public bool UseTemporary; public double Temporary; /// /// True if it's necessary to prevent the value changed event from firing. /// public bool IgnoreValueChanged { get; set; } #region Dependency Property public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(int.MaxValue, OnMaximumPropertyChanged)); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(int?), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(0, OnValuePropertyChanged)); public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(0, OnMinimumPropertyChanged)); public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(StepValue), typeof(int), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(1)); public static readonly DependencyProperty UpdateOnInputProperty = DependencyProperty.Register(nameof(UpdateOnInput), typeof(bool), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(false, OnUpdateOnInputPropertyChanged)); public static readonly DependencyProperty DefaultValueIfEmptyProperty = DependencyProperty.Register(nameof(DefaultValueIfEmpty), typeof(int?), typeof(NullableIntegerBox), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty PropagateWheelEventProperty = DependencyProperty.Register(nameof(PropagateWheelEvent), typeof(bool), typeof(NullableIntegerBox), new PropertyMetadata(default(bool))); #endregion #region Property Accessor [Bindable(true), Category("Common")] public int Maximum { get => (int)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } [Bindable(true), Category("Common")] public int? Value { get => (int?)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } [Bindable(true), Category("Common")] public int Minimum { get => (int)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } /// /// The Increment/Decrement value. /// [Description("The Increment/Decrement value.")] public int StepValue { get => (int)GetValue(StepProperty); set => SetValue(StepProperty, value); } [Bindable(true), Category("Common")] public bool UpdateOnInput { get => (bool)GetValue(UpdateOnInputProperty); set => SetValue(UpdateOnInputProperty, value); } [Bindable(true), Category("Common")] public int? DefaultValueIfEmpty { get => (int?)GetValue(DefaultValueIfEmptyProperty); set => SetValue(DefaultValueIfEmptyProperty, value); } /// /// True if the wheel events should not be set as handled. /// [Bindable(true), Category("Behavior")] public bool PropagateWheelEvent { get => (bool)GetValue(PropagateWheelEventProperty); set => SetValue(PropagateWheelEventProperty, value); } #endregion #region Properties Changed private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var intBox = d as NullableIntegerBox; if (intBox?.Value > intBox?.Maximum) intBox.Value = intBox.Maximum; } private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not NullableIntegerBox box || _ignore) return; _ignore = true; if (box.Value > box.Maximum) { box.UseTemporary = false; box.Temporary = box.Maximum; box.Value = box.Maximum; } if (box.Value < box.Minimum) { box.UseTemporary = false; box.Temporary = box.Minimum; box.Value = box.Minimum; } _ignore = false; var value = box.Value.ToString(); if (!string.Equals(box.Text, value)) box.Text = value; if (!box.IgnoreValueChanged) box.RaiseValueChangedEvent(); } private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var intBox = d as NullableIntegerBox; if (intBox?.Value < intBox?.Minimum) intBox.Value = intBox.Minimum; } private static void OnUpdateOnInputPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((NullableIntegerBox)d).UpdateOnInput = (bool)e.NewValue; } #endregion static NullableIntegerBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(NullableIntegerBox), new FrameworkPropertyMetadata(typeof(NullableIntegerBox))); } #region Custom Events /// /// Create a custom routed event by first registering a RoutedEventID, this event uses the bubbling routing strategy. /// public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(nameof(ValueChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NullableIntegerBox)); /// /// Event raised when the numeric value is changed. /// public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } public void RaiseValueChangedEvent() { if (ValueChangedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(ValueChangedEvent); RaiseEvent(newEventArgs); } #endregion #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(OnPasting)); } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); Text = Value.ToString(); } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (e.Source is NullableIntegerBox) SelectAll(); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Only sets the focus if not clicking on the Up/Down buttons of a IntegerUpDown. if (e.OriginalSource is TextBlock or Border) return; if (IsKeyboardFocusWithin) return; e.Handled = true; Focus(); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { if (string.IsNullOrEmpty(e.Text)) { e.Handled = true; return; } if (!IsEntryAllowed(e.Text)) { e.Handled = true; return; } base.OnPreviewTextInput(e); } protected override void OnTextChanged(TextChangedEventArgs e) { if (!UpdateOnInput || string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) return; Value = int.TryParse(Text, out var value) ? value : new int?(); base.OnTextChanged(e); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (!UpdateOnInput) { if (string.IsNullOrEmpty(Text) || !IsTextAllowed(Text)) { Value = DefaultValueIfEmpty; return; } Value = int.TryParse(Text, out var value) ? value : new int?(); return; } Text = Value.ToString(); } protected override void OnKeyDown(KeyEventArgs e) { if (e.Key is Key.Enter or Key.Return) { e.Handled = true; MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } base.OnKeyDown(e); } protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); var step = Keyboard.Modifiers switch { ModifierKeys.Shift | ModifierKeys.Control => 50, ModifierKeys.Shift => 10, ModifierKeys.Control => 5, _ => StepValue }; Value = e.Delta > 0 ? Math.Min(Maximum, (Value ?? 0) + step) : Math.Max(Minimum, (Value ?? 0) - step); e.Handled = !PropagateWheelEvent; } #endregion #region Base Properties Changed private void OnPasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { var text = e.DataObject.GetData(typeof(string)) as string; if (!IsTextAllowed(text)) e.CancelCommand(); } else { e.CancelCommand(); } } #endregion #region Methods private bool IsEntryAllowed(string text) { //Only numbers. var regex = new Regex(@"^-|[0-9]$"); //Checks if it's a valid char based on the context. return regex.IsMatch(text); } private bool IsTextAllowed(string text) { return Minimum < 0 ? Regex.IsMatch(text, @"^[-]?(?:[0-9]{1,9})?$") : Regex.IsMatch(text, @"^(?:[0-9]{1,9})?$"); } #endregion } ================================================ FILE: ScreenToGif/Controls/NullableIntegerUpDown.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; namespace ScreenToGif.Controls; /// /// Integer only control with up and down buttons to change the value. /// public class NullableIntegerUpDown : NullableIntegerBox { #region Variables private RepeatButton _upButton; private RepeatButton _downButton; #endregion static NullableIntegerUpDown() { DefaultStyleKeyProperty.OverrideMetadata(typeof(NullableIntegerUpDown), new FrameworkPropertyMetadata(typeof(NullableIntegerUpDown))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _upButton = Template.FindName("UpButton", this) as RepeatButton; _downButton = Template.FindName("DownButton", this) as RepeatButton; if (_upButton != null) _upButton.Click += UpButton_Click; if (_downButton != null) _downButton.Click += DownButton_Click; } #region Event Handlers private void DownButton_Click(object sender, RoutedEventArgs e) { if (!Value.HasValue) { Value = Minimum; return; } if (Value > Minimum) Value -= StepValue; } private void UpButton_Click(object sender, RoutedEventArgs e) { if (!Value.HasValue) { Value = Minimum; return; } if (Value < Maximum) Value += StepValue; } #endregion } ================================================ FILE: ScreenToGif/Controls/PuncturedRect.cs ================================================ using System; using System.Windows; using System.Windows.Media; using System.Windows.Shapes; namespace ScreenToGif.Controls; public class PuncturedRect : Shape { #region Dependency properties public static readonly DependencyProperty InteriorProperty = DependencyProperty.Register("Interior", typeof(Rect), typeof(FrameworkElement), new FrameworkPropertyMetadata(new Rect(0, 0, 0, 0), FrameworkPropertyMetadataOptions.AffectsRender, null, CoerceRectInterior, false), null); public static readonly DependencyProperty ExteriorProperty = DependencyProperty.Register("Exterior", typeof(Rect), typeof(FrameworkElement), new FrameworkPropertyMetadata(new Rect(0, 0, double.MaxValue, double.MaxValue), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsRender, null, null, false), null); public Rect Interior { get => (Rect)GetValue(InteriorProperty); set => SetValue(InteriorProperty, value); } public Rect Exterior { get => (Rect)GetValue(ExteriorProperty); set => SetValue(ExteriorProperty, value); } #endregion private static object CoerceRectInterior(DependencyObject d, object value) { var pr = (PuncturedRect)d; var rcExterior = pr.Exterior; var rcProposed = (Rect)value; if (rcExterior.Width <= 0 || rcExterior.Height <= 0) return rcExterior; var left = Math.Max(rcProposed.Left, rcExterior.Left); var top = Math.Max(rcProposed.Top, rcExterior.Top); var width = Math.Min(rcProposed.Right, rcExterior.Right) - left; var height = Math.Min(rcProposed.Bottom, rcExterior.Bottom) - top; return new Rect(left, top, width, height); } #region Override protected override Geometry DefiningGeometry { get { var pthfExt = new PathFigure {StartPoint = Exterior.TopLeft}; pthfExt.Segments.Add(new LineSegment(Exterior.TopRight, false)); pthfExt.Segments.Add(new LineSegment(Exterior.BottomRight, false)); pthfExt.Segments.Add(new LineSegment(Exterior.BottomLeft, false)); pthfExt.Segments.Add(new LineSegment(Exterior.TopLeft, false)); var pthgExt = new PathGeometry(); pthgExt.Figures.Add(pthfExt); var rectIntSect = Rect.Intersect(Exterior, Interior); var pthfInt = new PathFigure {StartPoint = rectIntSect.TopLeft}; pthfInt.Segments.Add(new LineSegment(rectIntSect.TopRight, false)); pthfInt.Segments.Add(new LineSegment(rectIntSect.BottomRight, false)); pthfInt.Segments.Add(new LineSegment(rectIntSect.BottomLeft, false)); pthfInt.Segments.Add(new LineSegment(rectIntSect.TopLeft, false)); var pthgInt = new PathGeometry(); pthgInt.Figures.Add(pthfInt); var cmbg = new CombinedGeometry(GeometryCombineMode.Exclude, pthgExt, pthgInt); return cmbg; } } #endregion } ================================================ FILE: ScreenToGif/Controls/RadialPanel.cs ================================================ using System; using System.Windows; using System.Windows.Controls; namespace ScreenToGif.Controls; /// ///A panel that organizes it's inner elements in a circular fashion. /// public class RadialPanel : Panel { /// /// Measure each children and give as much room as they want. /// protected override Size MeasureOverride(Size availableSize) { foreach (UIElement elem in Children) { //Give Infinite size as the available size for all the children. elem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); } return base.MeasureOverride(availableSize); } /// /// Arrange all children based on the geometric equations for the circle. /// protected override Size ArrangeOverride(Size finalSize) { if (Children.Count == 0) return finalSize; var angle = 0d; //Degrees converted to Radian by multiplying with PI/180 var incrementalAngularSpace = (360.0 / Children.Count) * (Math.PI / 180); //An approximate radii based on the available size , obviusly a better approach is needed here. var radiusX = finalSize.Width / 2.4; var radiusY = finalSize.Height / 2.4; foreach (UIElement elem in Children) { //Calculate the point on the circle for the element. var childPoint = new Point(Math.Cos(angle) * radiusX, -Math.Sin(angle) * radiusY); //Offsetting the point to the available rectangular area which is FinalSize. var actualChildPoint = new Point(finalSize.Width / 2 + childPoint.X - elem.DesiredSize.Width / 2, finalSize.Height / 2 + childPoint.Y - elem.DesiredSize.Height / 2); //Call Arrange method on the child element by giving the calculated point as the placementPoint. elem.Arrange(new Rect(actualChildPoint.X, actualChildPoint.Y, elem.DesiredSize.Width, elem.DesiredSize.Height)); //Calculate the new _angle for the next element. angle += incrementalAngularSpace; } return finalSize; } } ================================================ FILE: ScreenToGif/Controls/RangeSlider.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; namespace ScreenToGif.Controls; /// /// Range Slider control. /// public class RangeSlider : Control { #region Variables private Slider _lowerSlider; private Slider _upperSlider; private Border _progressBorder; #endregion #region Dependency Properties public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(RangeSlider), new FrameworkPropertyMetadata(0d)); public static readonly DependencyProperty LowerValueProperty = DependencyProperty.Register(nameof(LowerValue), typeof(double), typeof(RangeSlider), new FrameworkPropertyMetadata(10d, LowerValue_PropertyChanged)); public static readonly DependencyProperty UpperValueProperty = DependencyProperty.Register(nameof(UpperValue), typeof(double), typeof(RangeSlider), new FrameworkPropertyMetadata(90d, UpperValue_PropertyChanged)); public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(RangeSlider), new FrameworkPropertyMetadata(100d)); public static readonly DependencyProperty DisableLowerValueProperty = DependencyProperty.Register(nameof(DisableLowerValue), typeof(bool), typeof(RangeSlider), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty TickPlacementProperty = DependencyProperty.Register(nameof(TickPlacement), typeof(TickPlacement), typeof(RangeSlider), new FrameworkPropertyMetadata(TickPlacement.None)); #endregion #region Properties /// /// Minimum value of the slider. /// public double Minimum { get => (double)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } /// /// Maximum value of the slider. /// public double Maximum { get => (double)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } /// /// Value of the lower Thumb. /// public double LowerValue { get => (double)GetValue(LowerValueProperty); set => SetValue(LowerValueProperty, value); } /// /// Value of the upper Thumb. /// public double UpperValue { get => (double)GetValue(UpperValueProperty); set => SetValue(UpperValueProperty, value); } /// /// True to disable the range of the slider. /// public bool DisableLowerValue { get => (bool)GetValue(DisableLowerValueProperty); set { SetValue(DisableLowerValueProperty, value); LowerValue = Minimum; if (_lowerSlider != null) _lowerSlider.Visibility = DisableLowerValue ? Visibility.Collapsed : Visibility.Visible; } } /// /// The Tick placement position. /// public TickPlacement TickPlacement { get => (TickPlacement)GetValue(TickPlacementProperty); set => SetValue(TickPlacementProperty, value); } #endregion #region Property Changed private static void LowerValue_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is RangeSlider range)) return; if (range.LowerValue < range.Minimum) range.LowerValue = range.Minimum; if (range.LowerValue > range.UpperValue) range.UpperValue = range.LowerValue; range.RaiseLowerValueChangedEvent(); } private static void UpperValue_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is RangeSlider range)) return; if (range.UpperValue > range.Maximum) range.UpperValue = range.Maximum; if (range.LowerValue > range.UpperValue) range.LowerValue = range.UpperValue; range.RaiseUpperValueChangedEvent(); } #endregion #region Custom Events public static readonly RoutedEvent LowerValueChangedEvent = EventManager.RegisterRoutedEvent(nameof(LowerValueChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RangeSlider)); public static readonly RoutedEvent UpperValueChangedEvent = EventManager.RegisterRoutedEvent(nameof(UpperValueChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RangeSlider)); public event RoutedEventHandler LowerValueChanged { add => AddHandler(LowerValueChangedEvent, value); remove => RemoveHandler(LowerValueChangedEvent, value); } public event RoutedEventHandler UpperValueChanged { add => AddHandler(UpperValueChangedEvent, value); remove => RemoveHandler(UpperValueChangedEvent, value); } public void RaiseLowerValueChangedEvent() { if (LowerValueChangedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(LowerValueChangedEvent); RaiseEvent(newEventArgs); } public void RaiseUpperValueChangedEvent() { if (UpperValueChangedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(UpperValueChangedEvent); RaiseEvent(newEventArgs); } #endregion static RangeSlider() { DefaultStyleKeyProperty.OverrideMetadata(typeof(RangeSlider), new FrameworkPropertyMetadata(typeof(RangeSlider))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); LayoutUpdated += RangeSlider_LayoutUpdated; _lowerSlider = Template.FindName("LowerSlider", this) as Slider; _upperSlider = Template.FindName("UpperSlider", this) as Slider; _progressBorder = Template.FindName("ProgressBorder", this) as Border; if (_lowerSlider != null) { _lowerSlider.Value = LowerValue; _lowerSlider.PreviewMouseUp += LowerSlider_MouseUp; } if (_upperSlider != null) { _upperSlider.Value = UpperValue; _upperSlider.PreviewMouseUp += UpperSlider_PreviewMouseUp; } } protected override void OnPreviewKeyDown(KeyEventArgs e) { switch (e.Key) { case Key.Up: { e.Handled = true; UpperValue += 1; break; } case Key.Down: { e.Handled = true; UpperValue -= 1; break; } case Key.Right: { e.Handled = true; LowerValue += 1; break; } case Key.Left: { e.Handled = true; LowerValue -= 1; break; } } base.OnKeyDown(e); } private void SetProgressBorder() { if (Maximum - Minimum < 1) return; var lowerPoint = ActualWidth * (LowerValue - Minimum) / (Maximum - Minimum); var upperPoint = ActualWidth * (UpperValue - Minimum) / (Maximum - Minimum); upperPoint = ActualWidth - upperPoint; _progressBorder.Margin = new Thickness(lowerPoint, 0, upperPoint, 0); } #region Event Handlers private void UpperSlider_PreviewMouseUp(object sender, MouseButtonEventArgs e) { UpperValue = Math.Max(_upperSlider.Value, _lowerSlider.Value); SetProgressBorder(); } private void LowerSlider_MouseUp(object sender, MouseButtonEventArgs e) { LowerValue = Math.Min(_upperSlider.Value, _lowerSlider.Value); SetProgressBorder(); } private void RangeSlider_LayoutUpdated(object sender, EventArgs e) { SetProgressBorder(); } #endregion } ================================================ FILE: ScreenToGif/Controls/ResizingAdorner.cs ================================================ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace ScreenToGif.Controls; /// /// The Resizing Adorner controls. https://social.msdn.microsoft.com/Forums/vstudio/en-US/274bc547-dadf-42b5-b3f1-6d29407f9e79/resize-adorner-scale-problem?forum=wpf /// public class ResizingAdorner : Adorner { #region Variables /// /// Resizing adorner uses Thumbs for visual elements. /// The Thumbs have built-in mouse input handling. /// private readonly Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _middleBottom, _middleTop, _leftMiddle, _rightMiddle; /// /// The dashed border. /// private Rectangle _rectangle; /// /// To store and manage the adorner's visual children. /// readonly VisualCollection _visualChildren; /// /// The current adorned element. /// private UIElement _adornedElement; /// /// The parent of the element. /// private readonly UIElement _parent; /// /// The latest position of the element. Used by the drag operation. /// private Point _lastestPosition; #endregion /// /// Initialize the ResizingAdorner. /// /// The element to be adorned. /// True if it's available the drag to move action. /// The parent of the element. /// The start position of the first click. public ResizingAdorner(UIElement adornedElement, bool isMovable = true, UIElement parent = null, Point? startPosition = null) : base(adornedElement) { _visualChildren = new VisualCollection(this); #region Default values _adornedElement = adornedElement; _parent = parent ?? (_adornedElement as FrameworkElement)?.Parent as UIElement; if (startPosition.HasValue) _lastestPosition = startPosition.Value; #endregion //Creates the dashed rectangle around the adorned element. BuildAdornerBorder(); //Call a helper method to initialize the Thumbs with a customized cursors. BuildAdornerCorner(ref _topLeft, Cursors.SizeNWSE); BuildAdornerCorner(ref _topRight, Cursors.SizeNESW); BuildAdornerCorner(ref _bottomLeft, Cursors.SizeNESW); BuildAdornerCorner(ref _bottomRight, Cursors.SizeNWSE); BuildAdornerCorner(ref _middleBottom, Cursors.SizeNS); BuildAdornerCorner(ref _middleTop, Cursors.SizeNS); BuildAdornerCorner(ref _leftMiddle, Cursors.SizeWE); BuildAdornerCorner(ref _rightMiddle, Cursors.SizeWE); //Drag to move. if (isMovable) { _adornedElement.PreviewMouseLeftButtonDown += AdornedElement_PreviewMouseLeftButtonDown; _adornedElement.MouseMove += AdornedElement_MouseMove; _adornedElement.MouseUp += AdornedElement_MouseUp; } //Add handlers for resizing • Corners _bottomLeft.DragDelta += HandleBottomLeft; _bottomRight.DragDelta += HandleBottomRight; _topLeft.DragDelta += HandleTopLeft; _topRight.DragDelta += HandleTopRight; //Add handlers for resizing • Sides _middleBottom.DragDelta += HandleBottomMiddle; _middleTop.DragDelta += HandleTopMiddle; _leftMiddle.DragDelta += HandleLeftMiddle; _rightMiddle.DragDelta += HandleRightMiddle; } private void AdornedElement_MouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs) { _adornedElement?.ReleaseMouseCapture(); } private void AdornedElement_MouseMove(object sender, MouseEventArgs e) { if (_parent == null) return; if (_adornedElement is Image && e.LeftButton == MouseButtonState.Pressed) { _adornedElement.MouseMove -= AdornedElement_MouseMove; var currentPosition = e.GetPosition(_parent); Canvas.SetLeft(_adornedElement, Canvas.GetLeft(_adornedElement) + (currentPosition.X - _lastestPosition.X)); Canvas.SetTop(_adornedElement, Canvas.GetTop(_adornedElement) + (currentPosition.Y - _lastestPosition.Y)); _lastestPosition = currentPosition; _adornedElement.MouseMove += AdornedElement_MouseMove; } } private void AdornedElement_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (_parent == null) return; if (_adornedElement is Image && _adornedElement.CaptureMouse()) _lastestPosition = e.GetPosition(_parent); } #region DragDelta Event Handlers /// /// Handler for resizing from the bottom-right. /// private void HandleBottomRight(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; var zoomFactor = GetCanvasZoom(AdornedElement); // Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); // Change the size by the amount the user drags the mouse, as long as it's larger // than the width or height of an adorner, respectively. adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height); //Adjust canvas size. if (adornedElement.Parent is FrameworkElement canvas) { //Right. var elementLeft = Canvas.GetLeft(adornedElement); var elementRight = elementLeft + adornedElement.Width; if (elementRight > canvas.Width) canvas.Width = elementRight; //Bottom. var elementTop = Canvas.GetTop(adornedElement); var elementBottom = elementTop + adornedElement.Height; if (elementBottom > canvas.Height) canvas.Height = elementBottom; } } /// /// Handler for resizing from the top-right. /// private void HandleTopRight(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; // Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively. adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); var heightOld = adornedElement.Height; var heightNew = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height); var topOld = Canvas.GetTop(adornedElement); adornedElement.Height = heightNew; Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld)); //Adjust Canvas Right. var elementLeft = Canvas.GetLeft(adornedElement); var elementRight = elementLeft + adornedElement.Width; if (adornedElement.Parent is FrameworkElement canvas && elementRight > canvas.Width) canvas.Width = elementRight; } /// /// Handler for resizing from the top-left. /// private void HandleTopLeft(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; //Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively. var widthOld = adornedElement.Width; var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); var leftOld = Canvas.GetLeft(adornedElement); adornedElement.Width = widthNew; Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld)); var heightOld = adornedElement.Height; var heightNew = Math.Max(adornedElement.Height - args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height); var topOld = Canvas.GetTop(adornedElement); adornedElement.Height = heightNew; Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld)); } /// /// Handler for resizing from the bottom-left. /// private void HandleBottomLeft(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; //Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively. adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange /zoomFactor, hitThumb.DesiredSize.Height); var widthOld = adornedElement.Width; var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); var leftOld = Canvas.GetLeft(adornedElement); adornedElement.Width = widthNew; Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld)); //Adjust Canvas Bottom. var elementTop = Canvas.GetTop(adornedElement); var elementBottom = elementTop + adornedElement.Height; if (adornedElement.Parent is FrameworkElement canvas && elementBottom > canvas.Height) canvas.Height = elementBottom; } /// /// Handler for resizing from the bottom-middle. /// private void HandleBottomMiddle(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; // Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); //Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner. adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height); //Adjust Canvas Bottom. var elementTop = Canvas.GetTop(adornedElement); var elementBottom = elementTop + adornedElement.Height; if (adornedElement.Parent is FrameworkElement canvas && elementBottom > canvas.Height) canvas.Height = elementBottom; } /// /// Handler for resizing from the top-middle. /// private void HandleTopMiddle(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; // Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); // Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner. var heightOld = adornedElement.Height; var heightNew = Math.Max(adornedElement.Height - args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height); var topOld = Canvas.GetTop(adornedElement); adornedElement.Height = heightNew; Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld)); } /// /// Handler for resizing from the left-middle. /// private void HandleLeftMiddle(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; // Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); // Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner. var widthOld = adornedElement.Width; var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); var leftOld = Canvas.GetLeft(adornedElement); adornedElement.Width = widthNew; Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld)); } /// /// Handler for resizing from the right-middle. /// private void HandleRightMiddle(object sender, DragDeltaEventArgs args) { if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb) return; //Ensure that the Width and Height are properly initialized after the resize. EnforceSize(adornedElement); var zoomFactor = GetCanvasZoom(AdornedElement); //Change the size by the amount the user drags the mouse, as long as it's larger than the width of the adorner. adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width); //Adjust Canvas Right. var elementLeft = Canvas.GetLeft(adornedElement); var elementRight = elementLeft + adornedElement.Width; if (adornedElement.Parent is FrameworkElement canvas && elementRight > canvas.Width) canvas.Width = elementRight; } #endregion #region Private Methods /// /// Arrange the Adorners. /// /// The final Size /// The final size protected override Size ArrangeOverride(Size finalSize) { //Width and height of the element that's being adorned. var desiredWidth = AdornedElement.DesiredSize.Width; var desiredHeight = AdornedElement.DesiredSize.Height; _topLeft.Arrange(new Rect(-desiredWidth / 2, -desiredHeight / 2, desiredWidth, desiredHeight)); _topRight.Arrange(new Rect(desiredWidth / 2, -desiredHeight / 2, desiredWidth, desiredHeight)); _bottomLeft.Arrange(new Rect(-desiredWidth / 2, desiredHeight / 2, desiredWidth, desiredHeight)); _bottomRight.Arrange(new Rect(desiredWidth / 2, desiredHeight / 2, desiredWidth, desiredHeight)); _middleBottom.Arrange(new Rect(0, desiredHeight / 2, desiredWidth, desiredHeight)); _middleTop.Arrange(new Rect(0, -desiredHeight / 2, desiredWidth, desiredHeight)); _leftMiddle.Arrange(new Rect(-desiredWidth / 2, 0, desiredWidth, desiredHeight)); _rightMiddle.Arrange(new Rect(desiredWidth / 2, 0, desiredWidth, desiredHeight)); var zoomFactor = GetCanvasZoom(AdornedElement); _rectangle.Arrange(new Rect(0, 0, desiredWidth * zoomFactor, desiredHeight * zoomFactor)); return finalSize; } /// /// Instantiates the corner Thumbs, setting the Cursor property, /// some appearance properties, and add the elements to the visual tree. /// /// The Thumb to Instantiate. /// The custom cursor. private void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor) { if (cornerThumb != null) return; cornerThumb = new Thumb { Cursor = customizedCursor }; cornerThumb.Height = cornerThumb.Width = 10; cornerThumb.Style = (Style)FindResource("ScrollBar.Thumb"); _visualChildren.Add(cornerThumb); } /// /// Creates the dashed border around the adorned element. /// private void BuildAdornerBorder() { _rectangle = new Rectangle(); _rectangle.StrokeDashArray.Add(5); _rectangle.Stroke = new SolidColorBrush(Color.FromRgb(171, 171, 171)); _rectangle.StrokeThickness = 1; _visualChildren.Add(_rectangle); } // This method ensures that the Widths and Heights are initialized. Sizing to content produces // Width and Height values of Double.NaN. Because this Adorner explicitly resizes, the Width and Height // need to be set first. It also sets the maximum size of the adorned element. private void EnforceSize(FrameworkElement adornedElement) { if (adornedElement.Width.Equals(Double.NaN)) adornedElement.Width = adornedElement.DesiredSize.Width; if (adornedElement.Height.Equals(Double.NaN)) adornedElement.Height = adornedElement.DesiredSize.Height; //if (adornedElement.Parent is FrameworkElement parent) //{ // adornedElement.MaxHeight = parent.ActualHeight; // adornedElement.MaxWidth = parent.ActualWidth; //} } // Override the VisualChildrenCount and GetVisualChild properties to interface with // the adorner's visual collection. protected override int VisualChildrenCount => _visualChildren.Count; /// /// Gets the VisualChildren at given position. /// /// The Index to look for. /// The VisualChildren protected override Visual GetVisualChild(int index) { return _visualChildren[index]; } #endregion private double GetCanvasZoom(Visual referenceVisual) { if (referenceVisual is Canvas canvas1) return canvas1.LayoutTransform.Value.M11; var parent = VisualTreeHelper.GetParent(referenceVisual) as Visual; if (parent is Canvas canvas2) return canvas2.LayoutTransform.Value.M11; return 1; } public override GeneralTransform GetDesiredTransform(GeneralTransform transform) { var zoomFactor = GetCanvasZoom(AdornedElement); _topLeft.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _topRight.RenderTransformOrigin = new Point(0.5, 0.5); _topRight.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _topRight.RenderTransformOrigin = new Point(0.5, 0.5); _bottomLeft.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _bottomLeft.RenderTransformOrigin = new Point(0.5, 0.5); _bottomRight.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _bottomRight.RenderTransformOrigin = new Point(0.5, 0.5); _middleBottom.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _middleBottom.RenderTransformOrigin = new Point(0.5, 0.5); _middleTop.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _middleTop.RenderTransformOrigin = new Point(0.5, 0.5); _rightMiddle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _rightMiddle.RenderTransformOrigin = new Point(0.5, 0.5); _leftMiddle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); _leftMiddle.RenderTransformOrigin = new Point(0.5, 0.5); _rectangle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor); //_rectangle.RenderTransformOrigin = new Point(0.5, 0.5); return base.GetDesiredTransform(transform); } public void Destroy() { _adornedElement.PreviewMouseLeftButtonDown -= AdornedElement_PreviewMouseLeftButtonDown; _adornedElement.MouseMove -= AdornedElement_MouseMove; _adornedElement.MouseUp -= AdornedElement_MouseUp; _adornedElement = null; } } ================================================ FILE: ScreenToGif/Controls/SelectControl.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using Microsoft.Win32; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Models; using ScreenToGif.Native.External; using ScreenToGif.Util; using ScreenToGif.Util.Extensions; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; public class SelectControl : Control { #region Variables /// /// Resizing adorner uses Thumbs for visual elements. /// The Thumbs have built-in mouse input handling. /// private Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _top, _bottom, _left, _right; /// /// The selection rectangle, used to drag the selection Rect elsewhere. /// private Rectangle _rectangle; /// /// The grid that holds the three buttons to control the selection. /// private ExtendedUniformGrid _statusControlGrid; /// /// The pre-calculated size of the horizontal and vertical versions of the status control grid. /// private Size _statusHorizontalSize, _statusVerticalSize; /// /// The grids that holds the zoomed image and size info. /// private Grid _zoomGrid, _sizeGrid; //private readonly RegionMagnifier _regionMagnifier; /// /// The zoomed image. /// private Image _croppedImage; /// /// The textblock that lies at the bottom of the zoom view. /// private TextBlock _zoomTextBlock; /// /// The main canvas, the root element. /// private Canvas _mainCanvas; /// /// Status control buttons. /// private ExtendedButton _acceptButton, _retryButton, _cancelButton; ///// ///// The texblock that shows the size of the selection. ///// //private TextBlock _sizeTextBlock, _sizeNativeTextBlock; ///// ///// The grid that holds the sizing controls. ///// //private Grid _rectGrid; ///// ///// The button that closes the sizing widget. ///// //private ImageButton _closeRectButton; ///// ///// The grid that enables the movement of the sizing widget. ///// //private Grid _moveSizeWidgetGrid; /// /// The start point for the drag operation. /// private Point _startPoint; /// /// Blind spots for the ZoomView. If the cursor is on top of any of this spots, the zoom view should not appear. /// private readonly List _blindSpots = new List(); /// /// The latest window that contains the mouse cursor on top of it. /// private DetectedRegion _hitTestWindow; /// /// True when this control is ready to process mouse input when using the Screen/Window selection mode. /// This was added because the event MouseMove was being fired before the method that adjusts the other controls finished. (TL;DR It was a race condition) /// private bool _ready; /// /// True if the hover focus was changed to this selector. /// Other selectors must lose the hover focus. /// This makes the zoom view to be hidden everywhere else. /// private bool _wasHoverFocusChanged; public List Windows = new List(); public BitmapSource BackImage; #endregion #region Dependency Properties public static readonly DependencyProperty ParentLeftProperty = DependencyProperty.Register(nameof(ParentLeft), typeof(double), typeof(SelectControl), new PropertyMetadata(0d)); public static readonly DependencyProperty ParentTopProperty = DependencyProperty.Register(nameof(ParentTop), typeof(double), typeof(SelectControl), new PropertyMetadata(0d)); public static readonly DependencyProperty IsPickingRegionProperty = DependencyProperty.Register(nameof(IsPickingRegion), typeof(bool), typeof(SelectControl), new PropertyMetadata(true)); public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register(nameof(Selected), typeof(Rect), typeof(SelectControl), new PropertyMetadata(Rect.Empty, Selected_PropertyChanged)); public static readonly DependencyProperty NonExpandedSelectionProperty = DependencyProperty.Register(nameof(NonExpandedSelection), typeof(Rect), typeof(SelectControl), new PropertyMetadata(Rect.Empty)); public static readonly DependencyProperty NonExpandedNativeSelectionProperty = DependencyProperty.Register(nameof(NonExpandedNativeSelection), typeof(Rect), typeof(SelectControl), new PropertyMetadata(Rect.Empty)); public static readonly DependencyProperty FinishedSelectionProperty = DependencyProperty.Register(nameof(FinishedSelection), typeof(bool), typeof(SelectControl), new PropertyMetadata(false)); public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(nameof(Mode), typeof(ModeType), typeof(SelectControl), new PropertyMetadata(ModeType.Region)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(double), typeof(SelectControl), new PropertyMetadata(1d)); public static readonly DependencyProperty EmbeddedModeProperty = DependencyProperty.Register(nameof(EmbeddedMode), typeof(bool), typeof(SelectControl), new PropertyMetadata(false)); public static readonly DependencyProperty AnimateBorderProperty = DependencyProperty.Register(nameof(AnimateBorder), typeof(bool), typeof(SelectControl), new PropertyMetadata(false)); public static readonly RoutedEvent MouseHoveringEvent = EventManager.RegisterRoutedEvent(nameof(MouseHovering), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControl)); public static readonly RoutedEvent SelectionAcceptedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionAccepted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControl)); public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControl)); public static readonly RoutedEvent SelectionCanceledEvent = EventManager.RegisterRoutedEvent(nameof(SelectionCanceled), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControl)); #endregion #region Properties public double ParentLeft { get => (double)GetValue(ParentLeftProperty); set => SetValue(ParentLeftProperty, value); } public double ParentTop { get => (double)GetValue(ParentTopProperty); set => SetValue(ParentTopProperty, value); } public bool IsPickingRegion { get => (bool)GetValue(IsPickingRegionProperty); set => SetValue(IsPickingRegionProperty, value); } public Rect Selected { get => (Rect)GetValue(SelectedProperty); set => SetValue(SelectedProperty, value); } public Rect NonExpandedSelection { get => (Rect)GetValue(NonExpandedSelectionProperty); set => SetValue(NonExpandedSelectionProperty, value); } public Rect NonExpandedNativeSelection { get => (Rect)GetValue(NonExpandedNativeSelectionProperty); set => SetValue(NonExpandedNativeSelectionProperty, value); } public bool FinishedSelection { get => (bool)GetValue(FinishedSelectionProperty); set => SetValue(FinishedSelectionProperty, value); } public ModeType Mode { get => (ModeType)GetValue(ModeProperty); set => SetValue(ModeProperty, value); } public double Scale { get => (double)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } public bool EmbeddedMode { get => (bool)GetValue(EmbeddedModeProperty); set => SetValue(EmbeddedModeProperty, value); } public bool AnimateBorder { get => (bool)GetValue(AnimateBorderProperty); set => SetValue(AnimateBorderProperty, value); } public event RoutedEventHandler MouseHovering { add => AddHandler(MouseHoveringEvent, value); remove => RemoveHandler(MouseHoveringEvent, value); } public event RoutedEventHandler SelectionAccepted { add => AddHandler(SelectionAcceptedEvent, value); remove => RemoveHandler(SelectionAcceptedEvent, value); } public event RoutedEventHandler SelectionChanged { add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } public event RoutedEventHandler SelectionCanceled { add => AddHandler(SelectionCanceledEvent, value); remove => RemoveHandler(SelectionCanceledEvent, value); } #endregion static SelectControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectControl), new FrameworkPropertyMetadata(typeof(SelectControl))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _mainCanvas = Template.FindName("MainCanvas", this) as Canvas; _topLeft = Template.FindName("TopLeftThumb", this) as Thumb; _topRight = Template.FindName("TopRightThumb", this) as Thumb; _bottomLeft = Template.FindName("BottomLeftThumb", this) as Thumb; _bottomRight = Template.FindName("BottomRightThumb", this) as Thumb; _top = Template.FindName("TopThumb", this) as Thumb; _bottom = Template.FindName("BottomThumb", this) as Thumb; _left = Template.FindName("LeftThumb", this) as Thumb; _right = Template.FindName("RightThumb", this) as Thumb; _rectangle = Template.FindName("SelectRectangle", this) as Rectangle; _statusControlGrid = Template.FindName("StatusControlGrid", this) as ExtendedUniformGrid; _acceptButton = Template.FindName("AcceptButton", this) as ExtendedButton; _retryButton = Template.FindName("RetryButton", this) as ExtendedButton; _cancelButton = Template.FindName("CancelButton", this) as ExtendedButton; _zoomGrid = Template.FindName("ZoomGrid", this) as Grid; _croppedImage = Template.FindName("CroppedImage", this) as Image; _zoomTextBlock = Template.FindName("ZoomTextBlock", this) as TextBlock; _sizeGrid = Template.FindName("SizeGrid", this) as Grid; //_sizeTextBlock = Template.FindName("SizeTextBlock", this) as TextBlock; //_sizeNativeTextBlock = Template.FindName("NativeSizeTextBlock", this) as TextBlock; //_rectGrid = Template.FindName("RectGrid", this) as Grid; //_closeRectButton = Template.FindName("CloseSizeWidgetButton", this) as ImageButton; //_moveSizeWidgetGrid = Template.FindName("MoveSizeWidgetGrid", this) as Grid; Loaded += Control_Loaded; Unloaded += Control_Unloaded; SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; //Add handlers for resizing • Corners. _topLeft.DragDelta += HandleTopLeft; _topRight.DragDelta += HandleTopRight; _bottomLeft.DragDelta += HandleBottomLeft; _bottomRight.DragDelta += HandleBottomRight; //Add handlers for resizing • Sides. _top.DragDelta += HandleTop; _bottom.DragDelta += HandleBottom; _left.DragDelta += HandleLeft; _right.DragDelta += HandleRight; //Drag to move. _rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown; _rectangle.MouseMove += Rectangle_MouseMove; _rectangle.MouseLeftButtonUp += Rectangle_MouseLeftButtonUp; _acceptButton.Click += (sender, e) => { Accept(); }; _retryButton.Click += (sender, e) => { Retry(); }; _cancelButton.Click += (sender, e) => { Cancel(); }; //Replace with singleton property. //if (_regionMagnifier == null) //_regionMagnifier = new RegionMagnifier(); //Enable sizing controls. //if (!EmbeddedMode) //{ // _sizeTextBlock.PreviewMouseLeftButtonDown += SizeTextBlock_MouseUp; // _sizeTextBlock.IsHitTestVisible = true; // _sizeTextBlock.Cursor = Cursors.Hand; //} } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { _startPoint = e.GetPosition(this); if (Mode == ModeType.Region) { Selected = new Rect(e.GetPosition(this), new Size(0, 0)); FinishedSelection = false; CaptureMouse(); AdjustStatusControls(); AdjustFlowControls(); DetectBlindSpots(); } else { if (Selected.Width > 0 && Selected.Height > 0) { if (Mode == ModeType.Window && _hitTestWindow != null) User32.SetForegroundWindow(_hitTestWindow.Handle); Selected = Selected.Offset(-1); RaiseAcceptedEvent(); } } e.Handled = true; base.OnMouseLeftButtonDown(e); } protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) { if (Mode == ModeType.Region) Retry(); e.Handled = true; base.OnMouseLeftButtonDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (Mode == ModeType.Region) { var current = e.GetPosition(this); AdjustZoomView(current); if (!IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; // Move 1 pixel to current the position of the selection to the cursor. current.X++; current.Y++; if (current.X < -1) current.X = -1; if (current.Y < -1) current.Y = -1; if (current.X > ActualWidth) current.X = ActualWidth; if (current.Y > ActualHeight) current.Y = ActualHeight; Selected = new Rect(Math.Min(current.X, _startPoint.X), Math.Min(current.Y, _startPoint.Y), Math.Abs(current.X - _startPoint.X), Math.Abs(current.Y - _startPoint.Y)); AdjustInfo(current); } else if (_ready) { var current = e.GetPosition(this); _hitTestWindow = Windows.FirstOrDefault(x => x.Bounds.Contains(current)); Selected = _hitTestWindow?.Bounds ?? Rect.Empty; AdjustInfo(current); } base.OnMouseMove(e); } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if (Mode == ModeType.Region) { ReleaseMouseCapture(); if (Selected.Width < 30 || Selected.Height < 30) { OnMouseRightButtonDown(e); return; } FinishedSelection = true; AdjustThumbs(); AdjustStatusControls(e.GetPosition(this)); AdjustFlowControls(); DetectBlindSpots(); } //e.Handled = true; base.OnMouseLeftButtonUp(e); } protected override void OnPreviewKeyDown(KeyEventArgs e) { //Apparently, this event is not triggered. if (e.Key == Key.Escape) Cancel(); if (e.Key == Key.Enter || e.Key == Key.Return) Accept(); e.Handled = true; base.OnPreviewKeyDown(e); if (Mode != ModeType.Region || Selected.IsEmpty) return; var step = (Keyboard.Modifiers & ModifierKeys.Alt) != 0 ? 5 : 1; var key = e.Key == Key.System ? e.SystemKey : e.Key; //Control + Shift: Expand both ways. if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && (Keyboard.Modifiers & ModifierKeys.Shift) != 0) { switch (key) { case Key.Up: HandleBottom(_bottom, new DragDeltaEventArgs(0, step)); HandleTop(_top, new DragDeltaEventArgs(0, -step)); break; case Key.Down: HandleBottom(_bottom, new DragDeltaEventArgs(0, -step)); HandleTop(_top, new DragDeltaEventArgs(0, step)); break; case Key.Left: HandleRight(_right, new DragDeltaEventArgs(-step, 0)); HandleLeft(_left, new DragDeltaEventArgs(step, 0)); break; case Key.Right: HandleRight(_right, new DragDeltaEventArgs(step, 0)); HandleLeft(_left, new DragDeltaEventArgs(-step, 0)); break; } } else if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) //If the Shift key is pressed, the sizing mode is enabled (bottom right). { switch (key) { case Key.Up: HandleBottom(_bottom, new DragDeltaEventArgs(0, -step)); break; case Key.Down: HandleBottom(_bottom, new DragDeltaEventArgs(0, step)); break; case Key.Left: HandleRight(_right, new DragDeltaEventArgs(-step, 0)); break; case Key.Right: HandleRight(_right, new DragDeltaEventArgs(step, 0)); break; } } else if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) //If the Control key is pressed, the sizing mode is enabled (top left). { switch (key) { case Key.Up: HandleTop(_top, new DragDeltaEventArgs(0, -step)); break; case Key.Down: HandleTop(_top, new DragDeltaEventArgs(0, step)); break; case Key.Left: HandleLeft(_left, new DragDeltaEventArgs(-step, 0)); break; case Key.Right: HandleLeft(_left, new DragDeltaEventArgs(step, 0)); break; } } else { switch (key) //If no other key is pressed, the movement mode is enabled. { case Key.Up: HandleCenter(new DragDeltaEventArgs(0, -step)); break; case Key.Down: HandleCenter(new DragDeltaEventArgs(0, step)); break; case Key.Left: HandleCenter(new DragDeltaEventArgs(-step, 0)); break; case Key.Right: HandleCenter(new DragDeltaEventArgs(step, 0)); break; } } } #endregion #region Methods private void AdjustSelection() { //If already opened with a region selected, treat as "already selected". if (Selected == Rect.Empty) return; FinishedSelection = true; var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); AdjustFlowControls(); DetectBlindSpots(); AdjustInfo(point); } private void AdjustThumbs() { //Top left. Canvas.SetLeft(_topLeft, Selected.Left - _topLeft.Width / 2); Canvas.SetTop(_topLeft, Selected.Top - _topLeft.Height / 2); //Top right. Canvas.SetLeft(_topRight, Selected.Right - _topRight.Width / 2); Canvas.SetTop(_topRight, Selected.Top - _topRight.Height / 2); //Bottom left. Canvas.SetLeft(_bottomLeft, Selected.Left - _bottomLeft.Width / 2); Canvas.SetTop(_bottomLeft, Selected.Bottom - _bottomLeft.Height / 2); //Bottom right. Canvas.SetLeft(_bottomRight, Selected.Right - _bottomRight.Width / 2); Canvas.SetTop(_bottomRight, Selected.Bottom - _bottomRight.Height / 2); //Top. Canvas.SetLeft(_top, Selected.Left + Selected.Width / 2 - _top.Width / 2); Canvas.SetTop(_top, Selected.Top - _top.Height / 2); //Left. Canvas.SetLeft(_left, Selected.Left - _left.Width / 2); Canvas.SetTop(_left, Selected.Top + Selected.Height / 2 - _left.Height / 2); //Right. Canvas.SetLeft(_right, Selected.Right - _right.Width / 2); Canvas.SetTop(_right, Selected.Top + Selected.Height / 2 - _right.Height / 2); //Bottom. Canvas.SetLeft(_bottom, Selected.Left + Selected.Width / 2 - _bottom.Width / 2); Canvas.SetTop(_bottom, Selected.Bottom - _bottom.Height / 2); } private void AdjustZoomView(Point point) { //_bottom.IsVisible if (BackImage == null || Mode != ModeType.Region || !UserSettings.All.Magnifier || (Selected.Width > 10 && Selected.Height > 10 && Selected.Offset(5).Contains(point)) || _blindSpots.Any(x => x.Contains(point))) { _zoomGrid.Visibility = Visibility.Hidden; return; } //If this selector got the hover, the other selectors must hide their zoom views. if (!_wasHoverFocusChanged) { _wasHoverFocusChanged = true; RaiseMouseHoveringEvent(); } var scaledPoint = point.Scale(Scale); var scaledSize = (int)Math.Round(15 * Scale, MidpointRounding.AwayFromZero); try { //When using multiple monitors, the mouse cursor can paqss to another screen. This makes sure that to only get a valid screen position. if (scaledPoint.X < 0 || scaledPoint.Y < 0 || scaledPoint.X + scaledSize > BackImage.PixelWidth || scaledPoint.Y + scaledSize > BackImage.PixelHeight) { _zoomGrid.Visibility = Visibility.Hidden; return; } //The image is already 7 pixels offset of the current position. _croppedImage.Source = new CroppedBitmap(BackImage, new Int32Rect((int)scaledPoint.X, (int)scaledPoint.Y, scaledSize, scaledSize)); } catch (Exception) { } var left = point.X + 20; var top = point.Y - _zoomGrid.ActualHeight - 20; //Right overflow, adjust to the left. if (ActualWidth - point.X < _zoomGrid.ActualWidth + 20) left = point.X - _zoomGrid.ActualWidth - 20; //Top overflow, adjust to the bottom. if (point.Y - _zoomGrid.ActualHeight - 20 < 0) top = point.Y + 20; Canvas.SetLeft(_zoomGrid, left); Canvas.SetTop(_zoomGrid, top); _zoomTextBlock.Text = $"X: {Math.Round(point.X + ParentLeft, 2)} ◇ Y: {Math.Round(point.Y + ParentTop, 2)}"; _zoomGrid.Visibility = Visibility.Visible; } private void AdjustZoomViewDetached(Point point) { //If it should not display the zoom view. if (BackImage == null || Mode != ModeType.Region || !UserSettings.All.Magnifier || (Selected.Width > 10 && Selected.Height > 10 && Selected.Offset(5).Contains(point)) || _blindSpots.Any(x => x.Contains(point))) { //_regionMagnifier.Hide(); return; } //If this selector got the hover, the other selectors must hide their zoom views. if (!_wasHoverFocusChanged) { _wasHoverFocusChanged = true; RaiseMouseHoveringEvent(); } #region Get the zoommed-in part of the image //var scaledPoint = point.Scale(Scale); //var scaledSize = (int)Math.Round(15 * Scale, MidpointRounding.AwayFromZero); try { //The image is already 7 pixels offset of the current position. //_regionMagnifier.Image = new CroppedBitmap(BackImage, new Int32Rect((int)scaledPoint.X, (int)scaledPoint.Y, scaledSize, scaledSize)); } catch (Exception) { } #endregion //if (!_regionMagnifier.IsVisible) // _regionMagnifier.Show(); #region Reposition the zoom view //var left = point.X + 20; //var top = point.Y - _regionMagnifier.ActualHeight - 20; ////Right overflow, adjust to the left. //if (ActualWidth - point.X < _regionMagnifier.ActualWidth + 20) // left = point.X - _regionMagnifier.ActualWidth - 20; ////Top overflow, adjust to the bottom. //if (point.Y - _regionMagnifier.ActualHeight - 20 < 0) // top = point.Y + 20; //_regionMagnifier.Left = left + ParentLeft; //_regionMagnifier.Top = top + ParentTop; //_regionMagnifier.LeftPosition = point.X + ParentLeft; //_regionMagnifier.TopPosition = point.Y + ParentTop; #endregion } private void AdjustStatusControls(Point? point = null) { if (_statusControlGrid == null) return; if (!FinishedSelection || EmbeddedMode) { _statusControlGrid.Visibility = Visibility.Hidden; return; } //Show the controls always closest to the given point, if there's no space on the current monitor, //try finding the second closest point, or else show inside the selection rectangle. if (!point.HasValue) return; //var absolutePoint = new Point(point.Value.X, point.Value.Y); //If there's no space at the sides, show inside the rectangle. if (Selected.Width > ActualWidth - _statusVerticalSize.Width * 2 && Selected.Height > ActualHeight - _statusHorizontalSize.Height * 2) { _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left + Selected.Width / 2 - _statusControlGrid.ActualWidth / 2); Canvas.SetTop(_statusControlGrid, Selected.Top + Selected.Height / 2 - _statusControlGrid.ActualHeight / 2); } else { //Out of 4 Points, get the one that is closest to the current mouse position. var distances = new[] { (Selected.TopLeft - point.Value).Length, (Selected.TopRight - point.Value).Length, (Selected.BottomLeft - point.Value).Length, (Selected.BottomRight - point.Value).Length }; var index = Array.IndexOf(distances, distances.Min()); const int margin = 10; var canTopLeft = Selected.Top > _statusHorizontalSize.Height + margin || Selected.Left > _statusVerticalSize.Width + margin; var canBottomLeft = ActualHeight - Selected.Bottom > _statusHorizontalSize.Height + margin || Selected.Left > _statusVerticalSize.Width + margin; switch (index) { case 0: //Top Left. if (Selected.Top > _statusHorizontalSize.Height + margin) { //On top. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left); Canvas.SetTop(_statusControlGrid, Selected.Top - _statusControlGrid.ActualHeight - margin); break; } else if (Selected.Left > _statusVerticalSize.Width + margin) { //To the left. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left - _statusControlGrid.ActualWidth - margin); Canvas.SetTop(_statusControlGrid, Selected.Top); break; } if (Selected.Width > Selected.Height && canBottomLeft) goto case 2; //Bottom left. else goto case 1; //Top right. case 1: //Top Right. if (Selected.Top > _statusHorizontalSize.Height + margin) { //On top. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right - _statusControlGrid.ActualWidth); Canvas.SetTop(_statusControlGrid, Selected.Top - _statusControlGrid.ActualHeight - margin); break; } else if (ActualWidth - Selected.Right > _statusVerticalSize.Width + margin) { //To the right. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right + margin); Canvas.SetTop(_statusControlGrid, Selected.Top); break; } if (Selected.Width > Selected.Height && !canTopLeft) goto case 3; //Bottom right. else goto case 0; //Top left. case 2: //Bottom Left. if (ActualHeight - Selected.Bottom > _statusHorizontalSize.Height + margin) { //On the bottom. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left.Clamp(0, ActualWidth - _statusControlGrid.ActualWidth)); Canvas.SetTop(_statusControlGrid, Selected.Bottom + margin); break; } else if (Selected.Left > _statusVerticalSize.Width + margin) { //To the left. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left - _statusControlGrid.ActualWidth - margin); Canvas.SetTop(_statusControlGrid, Selected.Bottom - _statusControlGrid.ActualHeight); break; } if (Selected.Width > Selected.Height && canTopLeft) goto case 0; //Top left. else goto case 3; //Bottom right. case 3: //Bottom Right. if (ActualHeight - Selected.Bottom > _statusHorizontalSize.Height + margin) { //On the bottom. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, (Selected.Right - _statusControlGrid.ActualWidth).Clamp(0, ActualWidth - _statusControlGrid.ActualWidth)); Canvas.SetTop(_statusControlGrid, Selected.Bottom + margin); break; } else if (ActualWidth - Selected.Right > _statusVerticalSize.Width + margin) { //To the right. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right + margin); Canvas.SetTop(_statusControlGrid, Selected.Bottom - _statusControlGrid.ActualHeight); break; } if (Selected.Width > Selected.Height && !canBottomLeft) goto case 1; //Top right. else goto case 2; //Bottom left. } } _statusControlGrid.Visibility = Visibility.Visible; } private void AdjustFlowControls() { if (_mainCanvas == null) return; foreach (var button in _mainCanvas.Children.OfType()) button.Visibility = FinishedSelection ? Visibility.Hidden : Visibility.Visible; } private void AdjustInfo(Point? point = null) { if (_sizeGrid == null) return; if (point == null || Selected.IsEmpty || Selected.Width < _sizeGrid.ActualWidth || Selected.Height < _sizeGrid.ActualHeight) { _sizeGrid.Visibility = Visibility.Hidden; return; } //Out of 4 Points, get the one that is farthest from the current mouse position. var distances = new[] { (Selected.TopLeft - point.Value).Length, (Selected.TopRight - point.Value).Length, (Selected.BottomLeft - point.Value).Length, (Selected.BottomRight - point.Value).Length }; var index = Array.IndexOf(distances, distances.Max()); switch (index) { case 0: Canvas.SetTop(_sizeGrid, Selected.Top); Canvas.SetLeft(_sizeGrid, Selected.Left); break; case 1: Canvas.SetTop(_sizeGrid, Selected.Top); Canvas.SetLeft(_sizeGrid, Selected.Right - _sizeGrid.ActualWidth); break; case 2: Canvas.SetTop(_sizeGrid, Selected.Bottom - _sizeGrid.ActualHeight); Canvas.SetLeft(_sizeGrid, Selected.Left); break; case 3: Canvas.SetTop(_sizeGrid, Selected.Bottom - _sizeGrid.ActualHeight); Canvas.SetLeft(_sizeGrid, Selected.Right - _sizeGrid.ActualWidth); break; } _sizeGrid.Visibility = Visibility.Visible; } private void DetectBlindSpots() { _blindSpots.Clear(); if (Mode != ModeType.Region || !UserSettings.All.Magnifier) return; //If nothing selected, only the Close button will appear. if (Selected.IsEmpty) // || !FinishedSelection) { _blindSpots.Add(new Rect(new Point(ActualWidth - 40, 0), new Size(40, 40))); return; } if (_statusControlGrid.Visibility == Visibility.Visible) _blindSpots.Add(new Rect(new Point(Canvas.GetLeft(_statusControlGrid), Canvas.GetTop(_statusControlGrid)), new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight))); } internal void Accept() { if (!FinishedSelection) return; //Selected = Selected.Offset(-1); RaiseAcceptedEvent(); } public void Retry() { Selected = Rect.Empty; FinishedSelection = false; AdjustStatusControls(); AdjustFlowControls(); DetectBlindSpots(); AdjustInfo(); } public void Cancel() { Selected = Rect.Empty; FinishedSelection = false; AdjustStatusControls(); DetectBlindSpots(); RaiseCanceledEvent(); } public void HideZoom() { _wasHoverFocusChanged = false; _zoomGrid.Visibility = Visibility.Hidden; //_regionMagnifier.Hide(); } public void RaiseMouseHoveringEvent() { if (MouseHoveringEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(MouseHoveringEvent)); } public void RaiseAcceptedEvent() { if (SelectionAcceptedEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionAcceptedEvent)); } public void RaiseChangedEvent() { if (SelectionChangedEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); } public void RaiseCanceledEvent() { if (SelectionCanceledEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionCanceledEvent)); } private void CalculateStatusGridSizes() { _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.UpdateLayout(); _statusVerticalSize = new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight); _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.UpdateLayout(); _statusHorizontalSize = new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight); } #endregion #region Events public void Control_Loaded(object o, RoutedEventArgs routedEventArgs) { _ready = false; Keyboard.Focus(this); _blindSpots.Clear(); if (EmbeddedMode) { var viewBox = new Viewbox { Height = Height, Width = Width, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = LocalizationHelper.Get("S.Recorder.SelectArea.Embedded"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(80), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, ClipToBounds = true } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, 0); Canvas.SetTop(viewBox, 0); Panel.SetZIndex(viewBox, 0); AdjustSelection(); return; } if (IsMouseOver) AdjustZoomView(Mouse.GetPosition(this)); CalculateStatusGridSizes(); #region Close button //Close button. var button = new ExtendedButton { Name = "CancelButton", Width = 40, Height = 40, ContentHeight = 25, ContentWidth = 25, ToolTip = LocalizationHelper.Get("S.Recorder.CancelSelection"), Icon = TryFindResource("Vector.Cancel") as Brush, Style = TryFindResource("Style.Button.NoText.White") as Style, Cursor = Cursors.Arrow, Tag = "T" }; button.Click += (sender, e) => { Cancel(); }; _mainCanvas.Children.Add(button); Canvas.SetLeft(button, ActualWidth - 40); Canvas.SetTop(button, 0); Panel.SetZIndex(button, 8); _blindSpots.Add(new Rect(new Point(ActualWidth - 40, 0), new Size(40, 40))); #endregion if (Mode == ModeType.Fullscreen) { var viewBox = new Viewbox { Height = ActualHeight, Width = ActualWidth, Stretch = Stretch.Uniform, Tag = "T", IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = "👆 " + LocalizationHelper.Get("S.Recorder.SelectScreen"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 3, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 72, FontWeight = FontWeights.SemiBold, Margin = new Thickness(50) } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, 0); Canvas.SetTop(viewBox, 0); Panel.SetZIndex(viewBox, 0); } else if (Mode == ModeType.Window) { foreach (var window in Windows) { var border = new Border { Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Height = window.Bounds.Height, Width = window.Bounds.Width, Child = new Viewbox { Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, VerticalAlignment = VerticalAlignment.Center, Child = new TextPath { IsHitTestVisible = false, Text = window.Bounds.Width < 400 || window.Bounds.Height < 100 ? "👆" : "👆 " + LocalizationHelper.Get("S.Recorder.SelectWindow"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 3, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(20), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, } } }; border.UpdateLayout(); var top = Windows.Where(x => x.Order < window.Order).Select(x => x.Bounds).ToList(); var geo = new RectangleGeometry { Rect = new Rect(new Size(window.Bounds.Width, window.Bounds.Height)) }.GetFlattenedPathGeometry(0, ToleranceType.Absolute); if (top.Any()) { foreach (var region in top) { geo = Geometry.Combine(geo, new RectangleGeometry { Rect = new Rect(new Point(region.X - window.Bounds.X, region.Y - window.Bounds.Y), new Size(region.Width, region.Height)) }, GeometryCombineMode.Exclude, Transform.Identity); } border.Clip = geo; } _mainCanvas.Children.Insert(0, border); Canvas.SetLeft(border, window.Bounds.Left); Canvas.SetTop(border, window.Bounds.Top); Panel.SetZIndex(border, 0); } } else { var viewBox = new Viewbox { Height = ActualHeight, Width = ActualWidth, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = LocalizationHelper.Get("S.Recorder.SelectArea"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 3, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(80), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, ClipToBounds = true } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, 0); Canvas.SetTop(viewBox, 0); Panel.SetZIndex(viewBox, 0); } AdjustSelection(); _ready = true; //Triggers the mouse event to detect the mouse hit at start. OnMouseMove(new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)); } private void SystemEvents_DisplaySettingsChanged(object o, EventArgs eventArgs) { Scale = this.Scale(); } private void Control_Unloaded(object sender, RoutedEventArgs e) { SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; if (_mainCanvas == null) return; var list = _mainCanvas.Children.OfType().Where(x => x.Tag as string == "T").ToList(); foreach (var element in list) _mainCanvas.Children.Remove(element); //_regionMagnifier.Close(); } private static void Selected_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { if (!(o is SelectControl control)) return; //If nothing selected, simply ignore. if (control.Selected.IsEmpty) { control.NonExpandedSelection = control.Selected; control.NonExpandedNativeSelection = control.Selected; return; } //In a predetermined selection mode (window or screen) if (control.Mode == ModeType.Fullscreen || control.Mode == ModeType.Window) { control.NonExpandedSelection = control.Selected.Offset(0); //In this case Offset is just rounding the selection points. control.NonExpandedNativeSelection = control.Selected.Scale(control.Scale); control.RaiseChangedEvent(); return; } #region Region selection mode //For way too small regions, avoid applying the offset. That would throw an exception. if (control.Selected.Width < 5 || control.Selected.Height < 5) { control.NonExpandedSelection = control.Selected; control.NonExpandedNativeSelection = control.Selected; return; } control.NonExpandedSelection = control.Selected.Offset(1); control.NonExpandedNativeSelection = control.Selected.Scale(control.Scale).Offset(MathExtensions.RoundUpValue(control.Scale)); control.RaiseChangedEvent(); #endregion } private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (Mode != ModeType.Region) return; _startPoint = e.GetPosition(this); _rectangle.CaptureMouse(); FinishedSelection = false; AdjustStatusControls(); DetectBlindSpots(); AdjustInfo(); RaiseChangedEvent(); //Check if makes sense. e.Handled = true; } private void Rectangle_MouseMove(object sender, MouseEventArgs e) { if (Mode != ModeType.Region || !_rectangle.IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; //A quick double click will fire this event, when it should fire the OnMouseLeftButtonUp. if (Selected.IsEmpty || Selected.Width < 10 || Selected.Height < 10) return; _rectangle.MouseMove -= Rectangle_MouseMove; var currentPosition = e.GetPosition(this); var x = Selected.X + (currentPosition.X - _startPoint.X); var y = Selected.Y + (currentPosition.Y - _startPoint.Y); if (x < -1) x = -1; if (y < -1) y = -1; if (x + Selected.Width > ActualWidth + 1) x = ActualWidth + 1 - Selected.Width; if (y + Selected.Height > ActualHeight + 1) y = ActualHeight + 1 - Selected.Height; Selected = new Rect(x, y, Selected.Width, Selected.Height); _startPoint = currentPosition; e.Handled = true; AdjustInfo(); _rectangle.MouseMove += Rectangle_MouseMove; _zoomGrid.Visibility = Visibility.Collapsed; } private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (Mode != ModeType.Region) return; if (_rectangle.IsMouseCaptured) _rectangle?.ReleaseMouseCapture(); //A quick double quick will fire this event, when it should fire the OnMouseLeftButtonUp. if (Selected.IsEmpty || Selected.Width < 10 || Selected.Height < 10) return; FinishedSelection = true; var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); AdjustZoomView(point); e.Handled = true; } private void SizeTextBlock_MouseUp(object sender, MouseButtonEventArgs e) { //Open dialog asking for left/top/width/height. //_rectGrid.Visibility = Visibility.Visible; } /// ///Handler for resizing from the top-left. /// private void HandleTopLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the top-right. /// private void HandleTopRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the bottom-left. /// private void HandleBottomLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (left < 0) { width -= left * -1; left = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(left, Selected.Top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the bottom-right. /// private void HandleBottomRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the left-middle. /// private void HandleLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, Selected.Top, width, Selected.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the top-middle. /// private void HandleTop(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } Selected = new Rect(Selected.Left, top, Selected.Width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the right-middle. /// private void HandleRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, Selected.Top, width, Selected.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); DetectBlindSpots(); AdjustStatusControls(point); AdjustInfo(point); } /// /// Handler for resizing from the bottom-middle. /// private void HandleBottom(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, Selected.Width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for moving the selection. /// private void HandleCenter(DragDeltaEventArgs e) { e.Handled = true; var sel = new Rect(Selected.Left + e.HorizontalChange, Selected.Top + e.VerticalChange, Selected.Width, Selected.Height); #region Limit the drag to inside the bounds if (sel.Left < 0) sel.X = 0; if (sel.Top < 0) sel.Y = 0; if (sel.Right > ActualWidth) sel.X = ActualWidth - sel.Width; if (sel.Bottom > ActualHeight) sel.Y = ActualHeight - sel.Height; #endregion Selected = new Rect(sel.Left, sel.Top, sel.Width, sel.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } #endregion } ================================================ FILE: ScreenToGif/Controls/SelectControlOld.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using Microsoft.Win32; using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Models; using ScreenToGif.Domain.Models.Native; using ScreenToGif.Native.External; using ScreenToGif.Native.Helpers; using ScreenToGif.Util; using ScreenToGif.Util.Extensions; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; public class SelectControlOld : Control { #region Variables /// /// Resizing adorner uses Thumbs for visual elements. /// The Thumbs have built-in mouse input handling. /// private Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _top, _bottom, _left, _right; /// /// The selection rectangle, used to drag the selection Rect elsewhere. /// private Rectangle _rectangle; /// /// The grid that holds the three buttons to control the selection. /// private ExtendedUniformGrid _statusControlGrid; /// /// The pre-calculated size of the horizontal and vertical versions of the status control grid. /// private Size _statusHorizontalSize, _statusVerticalSize; /// /// The grid that holds the zoomed image. /// private Grid _zoomGrid; /// /// The zoomed image. /// private Image _croppedImage; /// /// The textblock that lies at the bottom of the zoom view. /// private TextBlock _zoomTextBlock; /// /// The main canvas, the root element. /// private Canvas _mainCanvas; /// /// Status control buttons. /// private ExtendedButton _acceptButton, _retryButton, _cancelButton; /// /// The texblock that shows the size of the selection. /// private TextBlock _sizeTextBlock; /// /// The grid that holds the sizing controls. /// private Grid _rectGrid; /// /// The button that closes the sizing widget. /// private ExtendedButton _closeRectButton; /// /// The grid that enables the movement of the sizing widget. /// private Grid _moveSizeWidgetGrid; /// /// The start point for the drag operation. /// private Point _startPoint; /// /// Blind spots for the ZoomView. If the cursor is on top of any of this spots, the zoom view should not appear. /// private readonly List _blindSpots = new List(); /// /// The latest window that contains the mouse cursor on top of it. /// private DetectedRegion _hitTestWindow; /// /// True when this control is ready to process mouse input when using the Screen/Window selection mode. /// This was added because the event MouseMove was being fired before the method that adjusts the other controls finished. (TL;DR It was a race condition) /// private bool _ready; public List Windows = new(); public List Monitors = new(); public BitmapSource BackImage; #endregion #region Dependency Properties public static readonly DependencyProperty IsPickingRegionProperty = DependencyProperty.Register(nameof(IsPickingRegion), typeof(bool), typeof(SelectControlOld), new PropertyMetadata(true)); public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register(nameof(Selected), typeof(Rect), typeof(SelectControlOld), new PropertyMetadata(Rect.Empty, Selected_PropertyChanged)); public static readonly DependencyProperty NonExpandedSelectionProperty = DependencyProperty.Register(nameof(NonExpandedSelection), typeof(Rect), typeof(SelectControlOld), new PropertyMetadata(Rect.Empty)); public static readonly DependencyProperty FinishedSelectionProperty = DependencyProperty.Register(nameof(FinishedSelection), typeof(bool), typeof(SelectControlOld), new PropertyMetadata(false)); public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(nameof(Mode), typeof(ModeType), typeof(SelectControlOld), new PropertyMetadata(ModeType.Region, Mode_Changed)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(double), typeof(SelectControlOld), new PropertyMetadata(1d, Mode_Changed)); public static readonly DependencyProperty EmbeddedModeProperty = DependencyProperty.Register(nameof(EmbeddedMode), typeof(bool), typeof(SelectControlOld), new PropertyMetadata(false)); public static readonly RoutedEvent SelectionAcceptedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionAccepted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControlOld)); public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControlOld)); public static readonly RoutedEvent SelectionCanceledEvent = EventManager.RegisterRoutedEvent(nameof(SelectionCanceled), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SelectControlOld)); #endregion #region Properties public bool IsPickingRegion { get => (bool)GetValue(IsPickingRegionProperty); set => SetValue(IsPickingRegionProperty, value); } public Rect Selected { get => (Rect)GetValue(SelectedProperty); set => SetValue(SelectedProperty, value); } public Rect NonExpandedSelection { get => (Rect)GetValue(NonExpandedSelectionProperty); set => SetValue(NonExpandedSelectionProperty, value); } public bool FinishedSelection { get => (bool)GetValue(FinishedSelectionProperty); set => SetValue(FinishedSelectionProperty, value); } public ModeType Mode { get => (ModeType)GetValue(ModeProperty); set => SetValue(ModeProperty, value); } public double Scale { get => (double)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } public bool EmbeddedMode { get => (bool)GetValue(EmbeddedModeProperty); set => SetValue(EmbeddedModeProperty, value); } public event RoutedEventHandler SelectionAccepted { add => AddHandler(SelectionAcceptedEvent, value); remove => RemoveHandler(SelectionAcceptedEvent, value); } public event RoutedEventHandler SelectionChanged { add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } public event RoutedEventHandler SelectionCanceled { add => AddHandler(SelectionCanceledEvent, value); remove => RemoveHandler(SelectionCanceledEvent, value); } #endregion static SelectControlOld() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectControlOld), new FrameworkPropertyMetadata(typeof(SelectControlOld))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _mainCanvas = Template.FindName("MainCanvas", this) as Canvas; _topLeft = Template.FindName("TopLeftThumb", this) as Thumb; _topRight = Template.FindName("TopRightThumb", this) as Thumb; _bottomLeft = Template.FindName("BottomLeftThumb", this) as Thumb; _bottomRight = Template.FindName("BottomRightThumb", this) as Thumb; _top = Template.FindName("TopThumb", this) as Thumb; _bottom = Template.FindName("BottomThumb", this) as Thumb; _left = Template.FindName("LeftThumb", this) as Thumb; _right = Template.FindName("RightThumb", this) as Thumb; _rectangle = Template.FindName("SelectRectangle", this) as Rectangle; _statusControlGrid = Template.FindName("StatusControlGrid", this) as ExtendedUniformGrid; _acceptButton = Template.FindName("AcceptButton", this) as ExtendedButton; _retryButton = Template.FindName("RetryButton", this) as ExtendedButton; _cancelButton = Template.FindName("CancelButton", this) as ExtendedButton; _zoomGrid = Template.FindName("ZoomGrid", this) as Grid; _croppedImage = Template.FindName("CroppedImage", this) as Image; _zoomTextBlock = Template.FindName("ZoomTextBlock", this) as TextBlock; _sizeTextBlock = Template.FindName("SizeTextBlock", this) as TextBlock; //_rectGrid = Template.FindName("RectGrid", this) as Grid; //_closeRectButton = Template.FindName("CloseSizeWidgetButton", this) as ImageButton; //_moveSizeWidgetGrid = Template.FindName("MoveSizeWidgetGrid", this) as Grid; //if (_topLeft == null || _topRight == null || _bottomLeft == null || _bottomRight == null || // _top == null || _bottom == null || _left == null || _right == null || _rectangle == null || _mainCanvas == null || _zoomGrid == null || _croppedImage == null) // return; Loaded += OnLoaded; Unloaded += OnUnloaded; SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; //Add handlers for resizing • Corners. _topLeft.DragDelta += HandleTopLeft; _topRight.DragDelta += HandleTopRight; _bottomLeft.DragDelta += HandleBottomLeft; _bottomRight.DragDelta += HandleBottomRight; //Add handlers for resizing • Sides. _top.DragDelta += HandleTop; _bottom.DragDelta += HandleBottom; _left.DragDelta += HandleLeft; _right.DragDelta += HandleRight; //Drag to move. _rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown; _rectangle.MouseMove += Rectangle_MouseMove; _rectangle.MouseLeftButtonUp += Rectangle_MouseLeftButtonUp; //if (_acceptButton == null || _retryButton == null || _cancelButton == null) // return; _acceptButton.Click += (sender, e) => { Accept(); }; _retryButton.Click += (sender, e) => { Retry(); }; _cancelButton.Click += (sender, e) => { Cancel(); }; //Enable sizing controls. //if (!EmbeddedMode) //{ // _sizeTextBlock.PreviewMouseLeftButtonDown += SizeTextBlock_MouseUp; // _sizeTextBlock.IsHitTestVisible = true; // _sizeTextBlock.Cursor = Cursors.Hand; //} Monitors = MonitorHelper.AllMonitorsScaled(Scale, true); } private void SystemEvents_DisplaySettingsChanged(object o, EventArgs eventArgs) { Scale = this.Scale(); Monitors = MonitorHelper.AllMonitorsScaled(Scale, true); //TODO: Adjust the selection and the UI when this happens. } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { _startPoint = e.GetPosition(this); if (Mode == ModeType.Region) { Selected = new Rect(e.GetPosition(this), new Size(0, 0)); FinishedSelection = false; CaptureMouse(); AdjustStatusControls(); AdjustFlowControls(); DetectBlindSpots(); } else { if (Mode == ModeType.Window && _hitTestWindow != null) User32.SetForegroundWindow(_hitTestWindow.Handle); if (Selected.Width > 0 && Selected.Height > 0) RaiseAcceptedEvent(); } e.Handled = true; base.OnMouseLeftButtonDown(e); } protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) { if (Mode == ModeType.Region) Retry(); e.Handled = true; base.OnMouseLeftButtonDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (Mode == ModeType.Region) { var current = e.GetPosition(this); AdjustZoomView(current); if (!IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; if (current.X < -1) current.X = -1; if (current.Y < -1) current.Y = -1; if (current.X > ActualWidth) current.X = ActualWidth; if (current.Y > ActualHeight) current.Y = ActualHeight; Selected = new Rect(Math.Min(current.X, _startPoint.X), Math.Min(current.Y, _startPoint.Y), Math.Abs(current.X - _startPoint.X), Math.Abs(current.Y - _startPoint.Y)); AdjustInfo(current); } else if (_ready) { var current = e.GetPosition(this); _hitTestWindow = Windows.FirstOrDefault(x => x.Bounds.Contains(current)); Selected = _hitTestWindow?.Bounds ?? Rect.Empty; AdjustInfo(current); } base.OnMouseMove(e); } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if (Mode == ModeType.Region) { ReleaseMouseCapture(); if (Selected.Width < 10 || Selected.Height < 10) { OnMouseRightButtonDown(e); return; } FinishedSelection = true; AdjustThumbs(); AdjustStatusControls(e.GetPosition(this)); AdjustFlowControls(); DetectBlindSpots(); } //e.Handled = true; base.OnMouseLeftButtonUp(e); } protected override void OnPreviewKeyDown(KeyEventArgs e) { //Apparently, this event is not triggered. if (e.Key == Key.Escape) Cancel(); if (e.Key == Key.Enter || e.Key == Key.Return) Accept(); e.Handled = true; base.OnPreviewKeyDown(e); if (Mode != ModeType.Region || Selected.IsEmpty) return; //Control + Shift: Expand both ways. if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && (Keyboard.Modifiers & ModifierKeys.Shift) != 0) { switch (e.Key) { case Key.Up: HandleBottom(_bottom, new DragDeltaEventArgs(0, 1)); HandleTop(_top, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleBottom(_bottom, new DragDeltaEventArgs(0, -1)); HandleTop(_top, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleRight(_right, new DragDeltaEventArgs(-1, 0)); HandleLeft(_left, new DragDeltaEventArgs(1, 0)); break; case Key.Right: HandleRight(_right, new DragDeltaEventArgs(1, 0)); HandleLeft(_left, new DragDeltaEventArgs(-1, 0)); break; } } else if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) //If the Shift key is pressed, the sizing mode is enabled (bottom right). { switch (e.Key) { case Key.Up: HandleBottom(_bottom, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleBottom(_bottom, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleRight(_right, new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleRight(_right, new DragDeltaEventArgs(1, 0)); break; } } else if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) //If the Control key is pressed, the sizing mode is enabled (top left). { switch (e.Key) { case Key.Up: HandleTop(_top, new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleTop(_top, new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleLeft(_left, new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleLeft(_left, new DragDeltaEventArgs(1, 0)); break; } } else { switch (e.Key) //If no other key is pressed, the movement mode is enabled. { case Key.Up: HandleCenter(new DragDeltaEventArgs(0, -1)); break; case Key.Down: HandleCenter(new DragDeltaEventArgs(0, 1)); break; case Key.Left: HandleCenter(new DragDeltaEventArgs(-1, 0)); break; case Key.Right: HandleCenter(new DragDeltaEventArgs(1, 0)); break; } } } #endregion #region Methods private void AdjustSelection() { //If already opened with a region selected, treat as "already selected". if (Selected == Rect.Empty) return; FinishedSelection = true; var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); AdjustFlowControls(); DetectBlindSpots(); AdjustInfo(point); } private void AdjustThumbs() { //Top left. Canvas.SetLeft(_topLeft, Selected.Left - _topLeft.Width / 2); Canvas.SetTop(_topLeft, Selected.Top - _topLeft.Height / 2); //Top right. Canvas.SetLeft(_topRight, Selected.Right - _topRight.Width / 2); Canvas.SetTop(_topRight, Selected.Top - _topRight.Height / 2); //Bottom left. Canvas.SetLeft(_bottomLeft, Selected.Left - _bottomLeft.Width / 2); Canvas.SetTop(_bottomLeft, Selected.Bottom - _bottomLeft.Height / 2); //Bottom right. Canvas.SetLeft(_bottomRight, Selected.Right - _bottomRight.Width / 2); Canvas.SetTop(_bottomRight, Selected.Bottom - _bottomRight.Height / 2); //Top. Canvas.SetLeft(_top, Selected.Left + Selected.Width / 2 - _top.Width / 2); Canvas.SetTop(_top, Selected.Top - _top.Height / 2); //Left. Canvas.SetLeft(_left, Selected.Left - _left.Width / 2); Canvas.SetTop(_left, Selected.Top + Selected.Height / 2 - _left.Height / 2); //Right. Canvas.SetLeft(_right, Selected.Right - _right.Width / 2); Canvas.SetTop(_right, Selected.Top + Selected.Height / 2 - _right.Height / 2); //Bottom. Canvas.SetLeft(_bottom, Selected.Left + Selected.Width / 2 - _bottom.Width / 2); Canvas.SetTop(_bottom, Selected.Bottom - _bottom.Height / 2); } private void AdjustZoomView(Point point) { if (BackImage == null || Mode != ModeType.Region || !UserSettings.All.Magnifier || (_bottom.IsVisible && Selected.Contains(point)) || _blindSpots.Any(x => x.Contains(point))) { _zoomGrid.Visibility = Visibility.Hidden; return; } var monitor = Monitors.FirstOrDefault(x => x.Bounds.Contains(point)); if (monitor == null) { _zoomGrid.Visibility = Visibility.Hidden; return; } var scaledPoint = point.Scale(Scale); var scaledSize = (int)Math.Round(15 * Scale, MidpointRounding.AwayFromZero); try { //The image is already 7 pixels offset of the current position. _croppedImage.Source = new CroppedBitmap(BackImage, new Int32Rect((int)scaledPoint.X, (int)scaledPoint.Y, scaledSize, scaledSize)); } catch (Exception) { } var left = point.X + 20; var top = point.Y - _zoomGrid.ActualHeight - 20; //Right overflow, adjust to the left. if (monitor.Bounds.Right - point.X < _zoomGrid.ActualWidth + 20) left = point.X - _zoomGrid.ActualWidth - 20; //Top overflow, adjust to the bottom. if (point.Y - _zoomGrid.ActualHeight - 20 < monitor.Bounds.Top) top = point.Y + 20; Canvas.SetLeft(_zoomGrid, left); Canvas.SetTop(_zoomGrid, top); _zoomTextBlock.Text = $"X: {scaledPoint.X + SystemParameters.VirtualScreenLeft} ◇ Y: {scaledPoint.Y + SystemParameters.VirtualScreenTop}"; _zoomGrid.Visibility = Visibility.Visible; } private void AdjustStatusControls(Point? point = null) { if (_statusControlGrid == null) return; if (!FinishedSelection || EmbeddedMode) { _statusControlGrid.Visibility = Visibility.Hidden; return; } //Show the controls always closest to the given point, if there's no space on the current monitor, //try finding the second closest point, or else show inside the selection rectangle. if (!point.HasValue) return; //If the main monitor is not the most left / top one, the bounds of monitors left to / above the main monitor are negative, //But the cursor point is always starting from 0,0 //So, the cursor point may not fall into any monitor bounds (exceed the maximum right / bottom coordinate) //As a result, convert the cursor point into the same axis of monitors by plusing the negative left / top coordinate //double minimumMonitorTop = Monitors.Min(x => x.Bounds.Top); //double minimumMonitorLeft = Monitors.Min(x => x.Bounds.Left); var absolutePoint = new Point(point.Value.X, point.Value.Y); var monitor = Monitors.FirstOrDefault(x => x.Bounds.Contains(absolutePoint)); if (monitor == null) return; //If there's no space at the sides, show inside the rectangle. if (Selected.Width > monitor.Bounds.Width - _statusVerticalSize.Width * 2 && Selected.Height > monitor.Bounds.Height - _statusHorizontalSize.Height * 2) { _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left + Selected.Width / 2 - _statusControlGrid.ActualWidth / 2); Canvas.SetTop(_statusControlGrid, Selected.Top + Selected.Height / 2 - _statusControlGrid.ActualHeight / 2); } else { //Out of 4 Points, get the one that is closest to the current mouse position. var distances = new[] { (Selected.TopLeft - point.Value).Length, (Selected.TopRight - point.Value).Length, (Selected.BottomLeft - point.Value).Length, (Selected.BottomRight - point.Value).Length }; var index = Array.IndexOf(distances, distances.Min()); const int margin = 10; var canTopLeft = Selected.Top - monitor.Bounds.Top > _statusHorizontalSize.Height + margin || Selected.Left - monitor.Bounds.Left > _statusVerticalSize.Width + margin; var canBottomLeft = monitor.Bounds.Bottom - Selected.Bottom > _statusHorizontalSize.Height + margin || Selected.Left - monitor.Bounds.Left > _statusVerticalSize.Width + margin; switch (index) { case 0: //Top Left. if (Selected.Top - monitor.Bounds.Top > _statusHorizontalSize.Height + margin) { //On top. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left); Canvas.SetTop(_statusControlGrid, Selected.Top - _statusControlGrid.ActualHeight - margin); break; } else if (Selected.Left - monitor.Bounds.Left > _statusVerticalSize.Width + margin) { //To the left. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left - _statusControlGrid.ActualWidth - margin); Canvas.SetTop(_statusControlGrid, Selected.Top); break; } if (Selected.Width > Selected.Height && canBottomLeft) goto case 2; //Bottom left. else goto case 1; //Top right. case 1: //Top Right. if (Selected.Top - monitor.Bounds.Top > _statusHorizontalSize.Height + margin) { //On top. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right - _statusControlGrid.ActualWidth); Canvas.SetTop(_statusControlGrid, Selected.Top - _statusControlGrid.ActualHeight - margin); break; } else if (monitor.Bounds.Right - Selected.Right > _statusVerticalSize.Width + margin) { //To the right. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right + margin); Canvas.SetTop(_statusControlGrid, Selected.Top); break; } if (Selected.Width > Selected.Height && canTopLeft) goto case 3; //Bottom right. else goto case 0; //Top left. case 2: //Bottom Left. if (monitor.Bounds.Bottom - Selected.Bottom > _statusHorizontalSize.Height + margin) { //On the bottom. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = false; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left); Canvas.SetTop(_statusControlGrid, Selected.Bottom + margin); break; } else if (Selected.Left - monitor.Bounds.Left > _statusVerticalSize.Width + margin) { //To the left. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Left - _statusControlGrid.ActualWidth - margin); Canvas.SetTop(_statusControlGrid, Selected.Bottom - _statusControlGrid.ActualHeight); break; } if (Selected.Width > Selected.Height && canTopLeft) goto case 0; //Top left. else goto case 3; //Bottom right. case 3: //Bottom Right. if (monitor.Bounds.Bottom - Selected.Bottom > _statusHorizontalSize.Height + margin) { //On the bottom. _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right - _statusControlGrid.ActualWidth); Canvas.SetTop(_statusControlGrid, Selected.Bottom + margin); break; } else if (monitor.Bounds.Right - Selected.Right > _statusVerticalSize.Width + margin) { //To the right. _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.IsReversed = true; _statusControlGrid.UpdateLayout(); Canvas.SetLeft(_statusControlGrid, Selected.Right + margin); Canvas.SetTop(_statusControlGrid, Selected.Bottom - _statusControlGrid.ActualHeight); break; } if (Selected.Width > Selected.Height && canBottomLeft) goto case 1; //Top right. else goto case 2; //Bottom left. } } _statusControlGrid.Visibility = Visibility.Visible; } private void AdjustFlowControls() { if (_mainCanvas == null) return; foreach (var button in _mainCanvas.Children.OfType()) button.Visibility = FinishedSelection ? Visibility.Hidden : Visibility.Visible; } private void AdjustInfo(Point? point = null) { if (_sizeTextBlock == null) return; if (point == null || Selected.IsEmpty || Selected.Width < _sizeTextBlock.ActualWidth || Selected.Height < _sizeTextBlock.ActualHeight) { _sizeTextBlock.Visibility = Visibility.Hidden; return; } //Out of 4 Points, get the one that is farthest from the current mouse position. var distances = new[] { (Selected.TopLeft - point.Value).Length, (Selected.TopRight - point.Value).Length, (Selected.BottomLeft - point.Value).Length, (Selected.BottomRight - point.Value).Length }; var index = Array.IndexOf(distances, distances.Max()); switch (index) { case 0: Canvas.SetTop(_sizeTextBlock, Selected.Top); Canvas.SetLeft(_sizeTextBlock, Selected.Left); break; case 1: Canvas.SetTop(_sizeTextBlock, Selected.Top); Canvas.SetLeft(_sizeTextBlock, Selected.Right - _sizeTextBlock.ActualWidth); break; case 2: Canvas.SetTop(_sizeTextBlock, Selected.Bottom - _sizeTextBlock.ActualHeight); Canvas.SetLeft(_sizeTextBlock, Selected.Left); break; case 3: Canvas.SetTop(_sizeTextBlock, Selected.Bottom - _sizeTextBlock.ActualHeight); Canvas.SetLeft(_sizeTextBlock, Selected.Right - _sizeTextBlock.ActualWidth); break; } _sizeTextBlock.Visibility = Visibility.Visible; } private void DetectBlindSpots() { _blindSpots.Clear(); if (Mode != ModeType.Region || !UserSettings.All.Magnifier) return; //If nothing selected, only the Close button will appear. if (Selected.IsEmpty)// || !FinishedSelection) { foreach (var monitor in Monitors) _blindSpots.Add(new Rect(new Point(monitor.Bounds.Right - 40, monitor.Bounds.Top), new Size(40, 40))); return; } if (_statusControlGrid.Visibility == Visibility.Visible) _blindSpots.Add(new Rect(new Point(Canvas.GetLeft(_statusControlGrid), Canvas.GetTop(_statusControlGrid)), new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight))); } internal void Accept() { if (!FinishedSelection) return; RaiseAcceptedEvent(); } public void Retry() { Selected = Rect.Empty; FinishedSelection = false; AdjustMode(); AdjustStatusControls(); AdjustFlowControls(); DetectBlindSpots(); AdjustInfo(); } public void Cancel() { Selected = Rect.Empty; FinishedSelection = false; AdjustStatusControls(); DetectBlindSpots(); RaiseCanceledEvent(); } public void RaiseAcceptedEvent() { if (SelectionAcceptedEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionAcceptedEvent)); } public void RaiseChangedEvent() { if (SelectionChangedEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); } public void RaiseCanceledEvent() { if (SelectionCanceledEvent == null || !IsLoaded) return; RaiseEvent(new RoutedEventArgs(SelectionCanceledEvent)); } public void AdjustMode() { if (Mode == ModeType.Window) Windows = WindowHelper.EnumerateWindows(Scale).AdjustPosition(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop); else if (Mode == ModeType.Fullscreen) Windows = MonitorHelper.AllMonitorsScaled(Scale, true).Select(x => new DetectedRegion(x.Handle, x.Bounds.Offset(-1), x.Name)).ToList(); else Windows.Clear(); } private void CalculateStatusGridSizes() { _statusControlGrid.Rows = 3; _statusControlGrid.Columns = 1; _statusControlGrid.UpdateLayout(); _statusVerticalSize = new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight); _statusControlGrid.Rows = 1; _statusControlGrid.Columns = 3; _statusControlGrid.UpdateLayout(); _statusHorizontalSize = new Size(_statusControlGrid.ActualWidth, _statusControlGrid.ActualHeight); } #endregion #region Events public void OnLoaded(object o, RoutedEventArgs routedEventArgs) { _ready = false; Keyboard.Focus(this); _blindSpots.Clear(); if (EmbeddedMode) { var viewBox = new Viewbox { Height = Height, Width = Width, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = LocalizationHelper.Get("S.Recorder.SelectArea.Embedded"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(80), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, ClipToBounds = true } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, 0); Canvas.SetTop(viewBox, 0); Panel.SetZIndex(viewBox, 0); AdjustSelection(); return; } AdjustZoomView(Mouse.GetPosition(this)); CalculateStatusGridSizes(); #region For each monitor foreach (var monitor in Monitors) { //Close button. var button = new ExtendedButton { Name = "CancelButton", Width = 40, Height = 40, ContentHeight = 25, ContentWidth = 25, ToolTip = LocalizationHelper.Get("S.Recorder.CancelSelection"), Icon = TryFindResource("Vector.Cancel") as Brush, Style = TryFindResource("Style.Button.NoText.White") as Style, Cursor = Cursors.Arrow, Tag = "T" }; button.Click += (sender, e) => { Cancel(); }; _mainCanvas.Children.Add(button); Canvas.SetLeft(button, monitor.Bounds.Right - 40); Canvas.SetTop(button, monitor.Bounds.Top); Panel.SetZIndex(button, 8); _blindSpots.Add(new Rect(new Point(monitor.Bounds.Right - 40, monitor.Bounds.Top), new Size(40, 40))); } #endregion if (Mode == ModeType.Fullscreen) { foreach (var monitor in Monitors) { var viewBox = new Viewbox { Height = monitor.Bounds.Height, Width = monitor.Bounds.Width, Stretch = Stretch.Uniform, Tag = "T", IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = "👆 " + LocalizationHelper.Get("S.Recorder.SelectScreen"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(50) } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, monitor.Bounds.Left); Canvas.SetTop(viewBox, monitor.Bounds.Top); Panel.SetZIndex(viewBox, 0); } } else if (Mode == ModeType.Window) { foreach (var window in Windows) { var border = new Border { Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Height = window.Bounds.Height, Width = window.Bounds.Width, Child = new Viewbox { Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, VerticalAlignment = VerticalAlignment.Center, Child = new TextPath { IsHitTestVisible = false, Text = window.Bounds.Width < 400 || window.Bounds.Height < 100 ? "👆" : "👆 " + LocalizationHelper.Get("S.Recorder.SelectWindow"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(20), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, } } }; var viewBox = new Viewbox { Height = window.Bounds.Height, Width = window.Bounds.Width, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, Tag = "T", ClipToBounds = true, IsHitTestVisible = false, VerticalAlignment = VerticalAlignment.Center, Child = new TextPath { IsHitTestVisible = false, Text = window.Bounds.Width < 400 || window.Bounds.Height < 100 ? "👆" : "👆 " + LocalizationHelper.Get("S.Recorder.SelectWindow"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(20), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, ClipToBounds = true, } //Child = new Border //{ // Background = PickBrush(), // Margin = new Thickness(20), // ClipToBounds = true, // VerticalAlignment = VerticalAlignment.Bottom, // HorizontalAlignment = HorizontalAlignment.Stretch, // Child = new TextPath // { // IsHitTestVisible = false, // Text = window.Bounds.Width < 400 || window.Bounds.Height < 100 ? "👆" // : "👆 " + this.TextResource("S.Recorder.SelectWindow"), // Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), // Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), // StrokeThickness = 1.6, // FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], // FontSize = 80, // FontWeight = FontWeights.SemiBold, // Margin = new Thickness(20), // VerticalAlignment = VerticalAlignment.Bottom, // HorizontalAlignment = HorizontalAlignment.Stretch, // ClipToBounds = true, // } //} }; border.UpdateLayout(); var top = Windows.Where(x => x.Order < window.Order).Select(x => x.Bounds).ToList(); var geo = new RectangleGeometry { Rect = new Rect(new Size(window.Bounds.Width, window.Bounds.Height)) }.GetFlattenedPathGeometry(0, ToleranceType.Absolute); if (top.Any()) { foreach (var region in top) { geo = Geometry.Combine(geo, new RectangleGeometry { Rect = new Rect(new Point(region.X - window.Bounds.X, region.Y - window.Bounds.Y), new Size(region.Width, region.Height)) }, GeometryCombineMode.Exclude, viewBox.RenderTransform); } border.Clip = geo; } _mainCanvas.Children.Insert(0, border); Canvas.SetLeft(border, window.Bounds.Left); Canvas.SetTop(border, window.Bounds.Top); Panel.SetZIndex(border, 0); } } else { foreach (var monitor in Monitors) { var viewBox = new Viewbox { Height = monitor.Bounds.Height, Width = monitor.Bounds.Width, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.Both, Tag = "T", ClipToBounds = true, IsHitTestVisible = false, Child = new TextPath { IsHitTestVisible = false, Text = LocalizationHelper.Get("S.Recorder.SelectArea"), Fill = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)), Stroke = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), StrokeThickness = 1.6, FontFamily = (FontFamily)Application.Current.Resources["FontFamilyNormal"], FontSize = 80, FontWeight = FontWeights.SemiBold, Margin = new Thickness(80), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, ClipToBounds = true } }; _mainCanvas.Children.Insert(0, viewBox); Canvas.SetLeft(viewBox, monitor.Bounds.Left); Canvas.SetTop(viewBox, monitor.Bounds.Top); Panel.SetZIndex(viewBox, 0); } } AdjustSelection(); _ready = true; } private void OnUnloaded(object sender, RoutedEventArgs e) { SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; if (_mainCanvas == null) return; var list = _mainCanvas.Children.OfType().Where(x => x.Tag as string == "T").ToList(); foreach (var element in list) _mainCanvas.Children.Remove(element); } private static void Selected_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { if (!(o is SelectControlOld control)) return; var rounded = MathExtensions.RoundUpValue(control.Scale); var width = Math.Round(control.Selected.Size.Width * control.Scale, MidpointRounding.AwayFromZero) - rounded * 2; var height = Math.Round(control.Selected.Size.Height * control.Scale, MidpointRounding.AwayFromZero) - rounded * 2; if (control.Selected.IsEmpty || height <= 0 || width <= 0) { control.NonExpandedSelection = control.Selected; return; } control.NonExpandedSelection = new Rect(control.Selected.TopLeft, control.Selected.Size).Scale(control.Scale).Offset(rounded); control.RaiseChangedEvent(); //Check if makes sense. } private static void Mode_Changed(DependencyObject o, DependencyPropertyChangedEventArgs e) { var control = o as SelectControlOld; control?.AdjustMode(); } private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (Mode != ModeType.Region) return; _startPoint = e.GetPosition(this); _rectangle.CaptureMouse(); FinishedSelection = false; AdjustStatusControls(); DetectBlindSpots(); AdjustInfo(); RaiseChangedEvent(); //Check if makes sense. e.Handled = true; } private void Rectangle_MouseMove(object sender, MouseEventArgs e) { if (Mode != ModeType.Region || !_rectangle.IsMouseCaptured || e.LeftButton != MouseButtonState.Pressed) return; //A quick double click will fire this event, when it should fire the OnMouseLeftButtonUp. if (Selected.IsEmpty || Selected.Width < 10 || Selected.Height < 10) return; _rectangle.MouseMove -= Rectangle_MouseMove; var currentPosition = e.GetPosition(this); var x = Selected.X + (currentPosition.X - _startPoint.X); var y = Selected.Y + (currentPosition.Y - _startPoint.Y); if (x < -1) x = -1; if (y < -1) y = -1; if (x + Selected.Width > ActualWidth + 1) x = ActualWidth + 1 - Selected.Width; if (y + Selected.Height > ActualHeight + 1) y = ActualHeight + 1 - Selected.Height; Selected = new Rect(x, y, Selected.Width, Selected.Height); _startPoint = currentPosition; e.Handled = true; AdjustInfo(); _rectangle.MouseMove += Rectangle_MouseMove; } private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (Mode != ModeType.Region) return; if (_rectangle.IsMouseCaptured) _rectangle?.ReleaseMouseCapture(); //A quick double quick will fire this event, when it should fire the OnMouseLeftButtonUp. if (Selected.IsEmpty || Selected.Width < 10 || Selected.Height < 10) return; FinishedSelection = true; var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); e.Handled = true; } private void SizeTextBlock_MouseUp(object sender, MouseButtonEventArgs e) { //Open dialog asking for left/top/width/height. _rectGrid.Visibility = Visibility.Visible; } /// ///Handler for resizing from the top-left. /// private void HandleTopLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the top-right. /// private void HandleTopRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the bottom-left. /// private void HandleBottomLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (left < 0) { width -= left * -1; left = 0; } if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(left, Selected.Top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the bottom-right. /// private void HandleBottomRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the left-middle. /// private void HandleLeft(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width - e.HorizontalChange, 10); var left = Selected.Left - (width - Selected.Width); if (left < 0) { width -= left * -1; left = 0; } Selected = new Rect(left, Selected.Top, width, Selected.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the top-middle. /// private void HandleTop(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height - e.VerticalChange, 10); var top = Selected.Top - (height - Selected.Height); if (top < 0) { height -= top * -1; top = 0; } Selected = new Rect(Selected.Left, top, Selected.Width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for resizing from the right-middle. /// private void HandleRight(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var width = Math.Max(Selected.Width + e.HorizontalChange, 10); if (Selected.Left + width > ActualWidth) width = ActualWidth - Selected.Left; Selected = new Rect(Selected.Left, Selected.Top, width, Selected.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); DetectBlindSpots(); AdjustStatusControls(point); AdjustInfo(point); } /// /// Handler for resizing from the bottom-middle. /// private void HandleBottom(object sender, DragDeltaEventArgs e) { if (!(sender is Thumb)) return; e.Handled = true; //Change the size by the amount the user drags the cursor. var height = Math.Max(Selected.Height + e.VerticalChange, 10); if (Selected.Top + height > ActualHeight) height = ActualHeight - Selected.Top; Selected = new Rect(Selected.Left, Selected.Top, Selected.Width, height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } /// /// Handler for moving the selection. /// private void HandleCenter(DragDeltaEventArgs e) { e.Handled = true; var sel = new Rect(Selected.Left + e.HorizontalChange, Selected.Top + e.VerticalChange, Selected.Width, Selected.Height); #region Limit the drag to inside the bounds if (sel.Left < 0) sel.X = 0; if (sel.Top < 0) sel.Y = 0; if (sel.Right > ActualWidth) sel.X = ActualWidth - sel.Width; if (sel.Bottom > ActualHeight) sel.Y = ActualHeight - sel.Height; #endregion Selected = new Rect(sel.Left, sel.Top, sel.Width, sel.Height); var point = Mouse.GetPosition(this); AdjustThumbs(); AdjustStatusControls(point); DetectBlindSpots(); AdjustInfo(point); } #endregion } ================================================ FILE: ScreenToGif/Controls/Shapes/Arrow.cs ================================================ using System.ComponentModel; using System.Windows; using System.Windows.Media; using System.Windows.Shapes; namespace ScreenToGif.Controls.Shapes; public sealed class Arrow : Shape { #region Dependency Properties public static readonly DependencyProperty X1Property = DependencyProperty.Register("X1", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty Y1Property = DependencyProperty.Register("Y1", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty X2Property = DependencyProperty.Register("X2", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty Y2Property = DependencyProperty.Register("Y2", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty HeadWidthProperty = DependencyProperty.Register("HeadWidth", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty HeadHeightProperty = DependencyProperty.Register("HeadHeight", typeof(double), typeof(Arrow), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); #endregion #region Properties [TypeConverter(typeof(LengthConverter))] public double X1 { get => (double)GetValue(X1Property); set => SetValue(X1Property, value); } [TypeConverter(typeof(LengthConverter))] public double Y1 { get => (double)GetValue(Y1Property); set => SetValue(Y1Property, value); } [TypeConverter(typeof(LengthConverter))] public double X2 { get => (double)GetValue(X2Property); set => SetValue(X2Property, value); } [TypeConverter(typeof(LengthConverter))] public double Y2 { get => (double)GetValue(Y2Property); set => SetValue(Y2Property, value); } [TypeConverter(typeof(LengthConverter))] public double HeadWidth { get => (double)GetValue(HeadWidthProperty); set => SetValue(HeadWidthProperty, value); } [TypeConverter(typeof(LengthConverter))] public double HeadHeight { get => (double)GetValue(HeadHeightProperty); set => SetValue(HeadHeightProperty, value); } #endregion #region Overrides protected override Geometry DefiningGeometry { get { //var vector = new Point(X2, Y2) - new Point(X1, Y1); //var angle = Vector.AngleBetween(new Vector(1, 0), vector); var geometry = new StreamGeometry(); var width = (double.IsNaN(Width) ? ActualWidth : Width) - StrokeThickness; var height = (double.IsNaN(Height) ? ActualHeight : Height) - StrokeThickness; using (var sgc = geometry.Open()) { //TODO: Add StrokeThickness / 2d to top left sgc.BeginFigure(new Point(width * 0.6898, height * 0.4), true, true); sgc.LineTo(new Point(width * 0, height * 0.4), true, true); sgc.LineTo(new Point(width * 0, height * 0.65), true, true); sgc.LineTo(new Point(width * 0.6898, height * 0.65), true, true); sgc.LineTo(new Point(width * 0.3684, height * 1), true, true); sgc.LineTo(new Point(width * 0.6608, height * 1), true, true); sgc.LineTo(new Point(width * 1, height * 0.5), true, true); sgc.LineTo(new Point(width * 0.6608, height * 0), true, true); sgc.LineTo(new Point(width * 0.3684, height * 0), true, true); } //geometry.Transform = new RotateTransform(angle, (Math.Abs(X1) - Math.Abs(X2)) / 2, (Math.Abs(Y1) - Math.Abs(Y2)) / 2); geometry.Freeze(); return geometry; } } #endregion } ================================================ FILE: ScreenToGif/Controls/Shapes/Triangle.cs ================================================ using System.Globalization; using System.Windows.Media; using System.Windows.Shapes; namespace ScreenToGif.Controls.Shapes; internal class Triangle : Shape { protected override Geometry DefiningGeometry => Geometry.Parse($"M {(Width/2d).ToString(CultureInfo.InvariantCulture)},{(StrokeThickness / 2d).ToString(CultureInfo.InvariantCulture)} " + $"L{(Width - (StrokeThickness / 2d)).ToString(CultureInfo.InvariantCulture)},{(Height - (StrokeThickness / 2d)).ToString(CultureInfo.InvariantCulture)} " + $"L {(StrokeThickness / 2d).ToString(CultureInfo.InvariantCulture)},{(Height - (StrokeThickness / 2d)).ToString(CultureInfo.InvariantCulture)} z"); } ================================================ FILE: ScreenToGif/Controls/SpectrumSlider.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using ScreenToGif.Util; namespace ScreenToGif.Controls; //Bug: If the user drags quickly the Thumb and release afterwards, the OnAfterSelection event is not triggered. #region SpectrumSlider /// /// Spectrum Slider. /// public class SpectrumSlider : Slider { #region Private Fields private ColorThumb _colorThumb; private Rectangle _spectrumRectangle; private LinearGradientBrush _pickerBrush; public delegate void AfterSelectingEventHandler(); public event AfterSelectingEventHandler AfterSelecting; #endregion #region Properties public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(SpectrumSlider), new PropertyMetadata(Colors.Transparent)); public static readonly DependencyProperty IsAlphaSpectrumProperty = DependencyProperty.Register(nameof(IsAlphaSpectrum), typeof(bool), typeof(SpectrumSlider), new PropertyMetadata(false)); public static readonly DependencyProperty SpectrumColorProperty = DependencyProperty.Register(nameof(SpectrumColor), typeof(Color), typeof(SpectrumSlider), new PropertyMetadata(default(Color), SpectrumColor_ChangedCallback)); /// /// Current selected Color. /// public Color SelectedColor { get => (Color)GetValue(SelectedColorProperty); set => SetValue(SelectedColorProperty, value); } /// /// True if the spectrum will display the same color but under different alpha values. /// public bool IsAlphaSpectrum { get => (bool)GetValue(IsAlphaSpectrumProperty); set => SetValue(IsAlphaSpectrumProperty, value); } /// /// The color used by the alpha sectrum. /// public Color SpectrumColor { get => (Color)GetValue(SpectrumColorProperty); set => SetValue(SpectrumColorProperty, value); } #endregion static SpectrumSlider() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SpectrumSlider), new FrameworkPropertyMetadata(typeof(SpectrumSlider))); } #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _spectrumRectangle = GetTemplateChild("PART_SpectrumDisplay") as Rectangle; _colorThumb = GetTemplateChild("Thumb") as ColorThumb; if (_colorThumb != null) { _colorThumb.PreviewMouseLeftButtonUp += ColorThumb_MouseLeftButtonUp; _colorThumb.MouseEnter += ColorThumb_MouseEnter; } UpdateColorSpectrum(); OnValueChanged(double.NaN, Value); } protected override void OnValueChanged(double oldValue, double newValue) { base.OnValueChanged(oldValue, newValue); SetValue(SelectedColorProperty, ColorExtensions.HsvToRgb(newValue, 1, 1, 255)); } #endregion #region Events private void ColorThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { AfterSelecting?.Invoke(); } private void ColorThumb_MouseEnter(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed && e.MouseDevice.Captured == null) { //https://social.msdn.microsoft.com/Forums/vstudio/en-US/5fa7cbc2-c99f-4b71-b46c-f156bdf0a75a/making-the-slider-slide-with-one-click-anywhere-on-the-slider?forum=wpf //The left button is pressed on mouse enter, but the mouse isn't captured, so the thumb //must have been moved under the mouse in response to a click on the track thanks to IsMoveToPointEnabled. //Generate a MouseLeftButtonDown event. _colorThumb.RaiseEvent(new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left) { RoutedEvent = MouseLeftButtonDownEvent }); } } private static void SpectrumColor_ChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { var box = d as SpectrumSlider; box?.UpdateColorSpectrum(); } #endregion #region Private Methods private void UpdateColorSpectrum() { if (_spectrumRectangle == null) return; _pickerBrush = new LinearGradientBrush { StartPoint = new Point(0.5, 0), EndPoint = new Point(0.5, 1), ColorInterpolationMode = ColorInterpolationMode.SRgbLinearInterpolation }; var colorsList = IsAlphaSpectrum ? ColorExtensions.GenerateAlphaSpectrum(SpectrumColor) : ColorExtensions.GenerateHsvSpectrum(40); var stopIncrement = 1d / colorsList.Count; var isDecimal = stopIncrement % 1 > 0; for (var i = 0; i < (isDecimal ? colorsList.Count - 1 : colorsList.Count); i++) _pickerBrush.GradientStops.Add(new GradientStop(colorsList[i], i * stopIncrement)); if (isDecimal) _pickerBrush.GradientStops.Add(new GradientStop(colorsList[colorsList.Count - 1], 1d)); _spectrumRectangle.Fill = _pickerBrush; } #endregion } #endregion #region HsvColor /// /// Describes a color in terms of Hue, Saturation, and Value (brightness) /// internal struct HsvColor { public double H; public double S; public double V; public HsvColor(double h, double s, double v) { H = h; S = s; V = v; } } #endregion #region ColorThumb /// /// The Thumb of the Spectrum Slider. /// public class ColorThumb : Thumb { static ColorThumb() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorThumb), new FrameworkPropertyMetadata(typeof(ColorThumb))); } public static readonly DependencyProperty ThumbColorProperty = DependencyProperty.Register(nameof(ThumbColor), typeof(Color), typeof(ColorThumb), new FrameworkPropertyMetadata(Colors.Transparent)); public static readonly DependencyProperty PointerOutlineThicknessProperty = DependencyProperty.Register(nameof(PointerOutlineThickness), typeof(double), typeof(ColorThumb), new FrameworkPropertyMetadata(1.0)); public static readonly DependencyProperty PointerOutlineBrushProperty = DependencyProperty.Register(nameof(PointerOutlineBrush), typeof(Brush), typeof(ColorThumb), new FrameworkPropertyMetadata(null)); /// /// The color of the Thumb. /// public Color ThumbColor { get => (Color)GetValue(ThumbColorProperty); set => SetValue(ThumbColorProperty, value); } public double PointerOutlineThickness { get => (double)GetValue(PointerOutlineThicknessProperty); set => SetValue(PointerOutlineThicknessProperty, value); } public Brush PointerOutlineBrush { get => (Brush)GetValue(PointerOutlineBrushProperty); set => SetValue(PointerOutlineBrushProperty, value); } } #endregion ================================================ FILE: ScreenToGif/Controls/SplitButton.cs ================================================ using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace ScreenToGif.Controls; public class SplitButton : ItemsControl { #region Variables private ExtendedButton _internalButton; private Popup _mainPopup; private ExtendedMenuItem _current; #endregion #region Dependency Properties public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(SplitButton), new PropertyMetadata("")); public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Brush), typeof(SplitButton)); public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(SplitButton), new FrameworkPropertyMetadata(16d)); public static readonly DependencyProperty ContentWidthProperty = DependencyProperty.Register(nameof(ContentWidth), typeof(double), typeof(SplitButton), new FrameworkPropertyMetadata(16d)); public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register(nameof(SelectedIndex), typeof(int), typeof(SplitButton), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender, SelectedIndex_ChangedCallback)); public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(SplitButton), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(SplitButton), new FrameworkPropertyMetadata(null)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(nameof(TextWrapping), typeof(TextWrapping), typeof(SplitButton), new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// The icon of the button as a Brush /// [Description("The icon of the button as a Brush.")] public Brush Icon { get => (Brush)GetValue(IconProperty); set => SetCurrentValue(IconProperty, value); } /// /// The height of the button content. /// [Description("The height of the button content."), Category("Common")] public double ContentHeight { get => (double)GetValue(ContentHeightProperty); set => SetCurrentValue(ContentHeightProperty, value); } /// /// The width of the button content. /// [Description("The width of the button content."), Category("Common")] public double ContentWidth { get => (double)GetValue(ContentWidthProperty); set => SetCurrentValue(ContentWidthProperty, value); } /// /// The index of selected item. /// [Description("The index of selected item."), Category("Common")] public int SelectedIndex { get => (int)GetValue(SelectedIndexProperty); set => SetCurrentValue(SelectedIndexProperty, value); } /// /// Gets or sets the command associated with the menu item. /// [Category("Action")] public ICommand Command { get => (ICommand) GetValue(CommandProperty); set => SetValue(CommandProperty, value); } /// /// Gets or sets the parameter to pass to the property. /// [Category("Action")] public object CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); } public TextWrapping TextWrapping { get => (TextWrapping)GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } #endregion static SplitButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitButton), new FrameworkPropertyMetadata(typeof(SplitButton))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _internalButton = Template.FindName("ActionButton", this) as ExtendedButton; _mainPopup = Template.FindName("Popup", this) as Popup; PrepareMainAction(this); //Raises the click event. _internalButton.Click += (sender, args) => _current?.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent)); //Close on click. foreach (var item in Items.OfType().ToList()) item.Click += (sender, args) => { _mainPopup.IsOpen = false; if (!(sender is ExtendedMenuItem menu)) return; var index = Items.OfType().Where(w => (w.Tag as string) != "I").ToList().IndexOf(menu); if (index != -1) SelectedIndex = index; }; } private static void SelectedIndex_ChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e) { if (!(o is SplitButton split) || !split.IsLoaded) return; split.PrepareMainAction(split); } private void PrepareMainAction(SplitButton split) { if (split.SelectedIndex < 0) return; //Ignore children with the Tag == "I". var list = split.Items.OfType().Where(w => (w.Tag as string) != "I").ToList(); if (split.SelectedIndex > list.Count - 1) { split.SelectedIndex = list.Count - 1; return; } //I'm using the Tag property to store the resource ID. if (list[split.SelectedIndex].Tag is string reference) split.SetResourceReference(TextProperty, reference); else split.Text = list[split.SelectedIndex].Header as string; split.Icon = list[split.SelectedIndex].Icon; split.Command = list[split.SelectedIndex].Command; _current = list[split.SelectedIndex]; } } ================================================ FILE: ScreenToGif/Controls/StatusBand.cs ================================================ using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media.Animation; using ScreenToGif.Domain.Enums; using Button = System.Windows.Controls.Button; using Control = System.Windows.Controls.Control; namespace ScreenToGif.Controls; public class StatusBand : Control { #region Variables private Grid _warningGrid; private Button _suppressButton; #endregion #region Dependency Properties/Events public static readonly DependencyProperty IdProperty = DependencyProperty.Register(nameof(Id), typeof(int), typeof(StatusBand), new FrameworkPropertyMetadata(0)); public static readonly DependencyProperty TypeProperty = DependencyProperty.Register(nameof(Type), typeof(StatusType), typeof(StatusBand), new FrameworkPropertyMetadata(StatusType.None)); public static readonly DependencyProperty ReasonProperty = DependencyProperty.Register(nameof(Reason), typeof(StatusReasons), typeof(StatusBand), new FrameworkPropertyMetadata(StatusReasons.None)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(StatusBand)); public static readonly DependencyProperty IsLinkProperty = DependencyProperty.Register(nameof(IsLink), typeof(bool), typeof(StatusBand), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty StartingProperty = DependencyProperty.Register(nameof(Starting), typeof(bool), typeof(StatusBand), new PropertyMetadata(default(bool))); public static readonly RoutedEvent DismissedEvent = EventManager.RegisterRoutedEvent(nameof(Dismissed), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(StatusBand)); #endregion #region Properties [Bindable(true), Category("Common")] public int Id { get => (int)GetValue(IdProperty); set => SetValue(IdProperty, value); } [Bindable(true), Category("Common")] public StatusType Type { get => (StatusType)GetValue(TypeProperty); set => SetValue(TypeProperty, value); } [Bindable(true), Category("Common")] public StatusReasons Reason { get => (StatusReasons)GetValue(ReasonProperty); set => SetValue(ReasonProperty, value); } [Bindable(true), Category("Common")] public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } [Bindable(true), Category("Common")] public bool IsLink { get => (bool)GetValue(IsLinkProperty); set => SetValue(IsLinkProperty, value); } /// /// True if started to display the message. /// [Bindable(true), Category("Common")] public bool Starting { get => (bool)GetValue(StartingProperty); set => SetValue(StartingProperty, value); } /// /// Event raised when the StatusBand gets dismissed/suppressed. /// public event RoutedEventHandler Dismissed { add => AddHandler(DismissedEvent, value); remove => RemoveHandler(DismissedEvent, value); } public Action Action { get; set; } #endregion static StatusBand() { DefaultStyleKeyProperty.OverrideMetadata(typeof(StatusBand), new FrameworkPropertyMetadata(typeof(StatusBand))); } public override void OnApplyTemplate() { _warningGrid = GetTemplateChild("WarningGrid") as Grid; var link = GetTemplateChild("MainHyperlink") as Hyperlink; _suppressButton = GetTemplateChild("SuppressButton") as ExtendedButton; if (_suppressButton != null) _suppressButton.Click += SuppressButton_Click; if (Action != null && link != null) link.Click += (sender, args) => Action.Invoke(); base.OnApplyTemplate(); } #region Methods public void Show(StatusType type, string text, Action action = null) { Action = action; //Collapsed-by-default elements do not apply templates. //http://stackoverflow.com/a/2115873/1735672 //So it's necessary to do this here. ApplyTemplate(); Starting = true; Type = type; Text = text; IsLink = action != null; if (_warningGrid?.FindResource("ShowWarningStoryboard") is Storyboard show) BeginStoryboard(show); } public void Update(string text, Action action = null) { Show(StatusType.Update, text, action); } public void Info(string text, Action action = null) { Show(StatusType.Info, text, action); } public void Warning(string text, Action action = null) { Show(StatusType.Warning, text, action); } public void Error(string text, Action action = null) { Show(StatusType.Error, text, action); } public void Hide() { Starting = false; if (_warningGrid?.Visibility == Visibility.Collapsed) return; if (_warningGrid?.FindResource("HideWarningStoryboard") is Storyboard hide) BeginStoryboard(hide); RaiseDismissedEvent(); } public void RaiseDismissedEvent() { if (DismissedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(DismissedEvent); RaiseEvent(newEventArgs); } public static string KindToString(StatusType kind) { return "Vector." + (kind == StatusType.None ? "Tag" : kind == StatusType.Info ? "Info" : kind == StatusType.Update ? "Synchronize" : kind == StatusType.Warning ? "Warning" : "Cancel.Round"); } #endregion private void SuppressButton_Click(object sender, RoutedEventArgs e) { Hide(); } } ================================================ FILE: ScreenToGif/Controls/StatusList.cs ================================================ using System; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using ScreenToGif.Domain.Enums; namespace ScreenToGif.Controls; public class StatusList : StackPanel { #region Dependency Properties/Events public static readonly DependencyProperty MaxBandsProperty = DependencyProperty.Register("MaxBands", typeof(int), typeof(StatusBand), new FrameworkPropertyMetadata(5)); #endregion #region Properties [Bindable(true), Category("Common")] public int MaxBands { get => (int)GetValue(MaxBandsProperty); set => SetValue(MaxBandsProperty, value); } #endregion private void Add(StatusType type, string text, StatusReasons reason, Action action = null) { var current = Children.OfType().FirstOrDefault(x => x.Type == type && x.Text == text); if (current != null) Children.Remove(current); var band = new StatusBand { Reason = reason }; band.Dismissed += (_, _) => Children.Remove(band); if (Children.Count >= MaxBands) Children.RemoveAt(0); Children.Add(band); switch (type) { case StatusType.Info: band.Info(text, action); break; case StatusType.Warning: band.Warning(text, action); break; case StatusType.Error: band.Error(text, action); break; } } public void Info(string text, StatusReasons reason = StatusReasons.None, Action action = null) { Add(StatusType.Info, text, reason, action); } public void Warning(string text, StatusReasons reason = StatusReasons.InvalidState, Action action = null) { Add(StatusType.Warning, text, reason, action); } public void Error(string text, StatusReasons reason, Action action = null) { Add(StatusType.Error, text, reason, action); } public void Remove(StatusType type, StatusReasons? reason = null) { var list = Children.OfType().Where(x => x.Type == type && (!reason.HasValue || x.Reason == reason)).ToList(); foreach (var band in list) Children.Remove(band); } public void Clear() { Children.Clear(); } } ================================================ FILE: ScreenToGif/Controls/TextPath.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Shapes; using ScreenToGif.Util; using ScreenToGif.Util.Settings; namespace ScreenToGif.Controls; /// /// /// This class generates a Geometry from a block of text in a specific font, weight, etc. and renders it to WPF as a shape. /// public class TextPath : Shape { /// /// Data member that holds the generated geometry /// private Geometry _textGeometry; private Pen _pen; #region Dependency Properties public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TextPath), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); public static readonly DependencyProperty OriginPointProperty = DependencyProperty.Register("Origin", typeof(Point), typeof(TextPath), new FrameworkPropertyMetadata(new Point(0.5, 0.5), FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TextPath), new FrameworkPropertyMetadata(SystemFonts.MessageFontFamily, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits)); public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(TextPath), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(typeof(TextPath), new FrameworkPropertyMetadata(TextElement.FontStretchProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits)); public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(typeof(TextPath), new FrameworkPropertyMetadata(SystemFonts.MessageFontStyle, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits)); public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(typeof(TextPath), new FrameworkPropertyMetadata(SystemFonts.MessageFontWeight, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.Inherits)); #endregion #region Property Accessors [Bindable(true), Category("Appearance")] [TypeConverter(typeof(PointConverter))] public Point Origin { get => (Point)GetValue(OriginPointProperty); set => SetValue(OriginPointProperty, value); } [Bindable(true), Category("Appearance")] [Localizability(LocalizationCategory.Font)] [TypeConverter(typeof(FontFamilyConverter))] public FontFamily FontFamily { get => (FontFamily)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } [Bindable(true), Category("Appearance")] [TypeConverter(typeof(FontSizeConverter))] [Localizability(LocalizationCategory.None)] public double FontSize { get => (double)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } [Bindable(true), Category("Appearance")] [TypeConverter(typeof(FontStretchConverter))] public FontStretch FontStretch { get => (FontStretch)GetValue(FontStretchProperty); set => SetValue(FontStretchProperty, value); } [Bindable(true), Category("Appearance")] [TypeConverter(typeof(FontStyleConverter))] public FontStyle FontStyle { get => (FontStyle)GetValue(FontStyleProperty); set => SetValue(FontStyleProperty, value); } [Bindable(true), Category("Appearance")] [TypeConverter(typeof(FontWeightConverter))] public FontWeight FontWeight { get => (FontWeight)GetValue(FontWeightProperty); set => SetValue(FontWeightProperty, value); } [Bindable(true), Category("Appearance")] public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } #endregion /// /// /// This method is called to retrieve the geometry that defines the shape. /// protected override Geometry DefiningGeometry => _textGeometry ?? Geometry.Empty; protected override void OnRender(DrawingContext drawingContext) { try { _textGeometry.Transform = new TranslateTransform(-_textGeometry.Bounds.X, -_textGeometry.Bounds.Y + 1); } catch (Exception) {} //If the outline of the text should not be rendered outside, use the base OnRender method. if (!UserSettings.All.DrawOutlineOutside) { base.OnRender(drawingContext); return; } //This code will draw the outline outside the text. drawingContext.DrawGeometry(null, _pen, _textGeometry); drawingContext.DrawGeometry(Fill, null, _textGeometry); } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (!IsVisible) { base.OnPropertyChanged(e); return; } try { _textGeometry = new FormattedText(Text ?? "", CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip).BuildGeometry(Origin); } catch (Exception ex) { LogWriter.Log(ex, "Impossible to build text geometry."); try { _textGeometry = new FormattedText(Text ?? "", CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface(new FontFamily("Arial"), FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip).BuildGeometry(Origin); } catch (Exception ex2) { LogWriter.Log(ex2, "Impossible to build text geometry with default font."); } } _pen = new Pen(Stroke, StrokeThickness) { DashCap = PenLineCap.Round, EndLineCap = PenLineCap.Round, LineJoin = PenLineJoin.Round, StartLineCap = PenLineCap.Round, MiterLimit = StrokeMiterLimit }; InvalidateVisual(); base.OnPropertyChanged(e); } protected override Size MeasureOverride(Size constraint) { var definingGeometry = DefiningGeometry; var dashStyle = (DashStyle)null; if (_pen != null) { dashStyle = _pen.DashStyle; if (dashStyle != null) _pen.DashStyle = null; } var renderBounds = definingGeometry.GetRenderBounds(_pen); if (dashStyle != null) _pen.DashStyle = dashStyle; return new Size(Math.Max(renderBounds.Right - renderBounds.X, 0.0), Math.Max(MinHeight, Math.Max(renderBounds.Bottom - renderBounds.Y + 1, 0.0))); } } ================================================ FILE: ScreenToGif/Controls/TimeBox.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Windows; using System.Windows.Input; namespace ScreenToGif.Controls; public class TimeBox : ExtendedTextBox { private bool _ignore = false; #region Dependency Properties public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register(nameof(Selected), typeof(TimeSpan?), typeof(TimeBox), new FrameworkPropertyMetadata(null, Selected_PropertyChanged)); public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(TimeSpan?), typeof(TimeBox), new FrameworkPropertyMetadata(new TimeSpan(0, 23, 59, 59, 999), Maximum_PropertyChanged)); public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(TimeSpan?), typeof(TimeBox), new FrameworkPropertyMetadata(TimeSpan.Zero, Minimum_PropertyChanged)); public static readonly DependencyProperty AvoidScrollProperty = DependencyProperty.Register(nameof(AvoidScroll), typeof(bool), typeof(TimeBox), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty DisplaySecondsProperty = DependencyProperty.Register(nameof(DisplaySeconds), typeof(bool), typeof(TimeBox), new FrameworkPropertyMetadata(true)); public static readonly DependencyProperty DisplayMillisecondsProperty = DependencyProperty.Register(nameof(DisplayMilliseconds), typeof(bool), typeof(TimeBox), new FrameworkPropertyMetadata(true)); public static readonly DependencyProperty DisplayEmptyAsMidnightProperty = DependencyProperty.Register(nameof(DisplayEmptyAsMidnight), typeof(bool), typeof(TimeBox), new FrameworkPropertyMetadata(false)); #endregion #region Property Accessor [Bindable(true), Category("Common")] public TimeSpan? Selected { get => (TimeSpan?)GetValue(SelectedProperty); set => SetValue(SelectedProperty, value); } [Bindable(true), Category("Common")] public TimeSpan? Maximum { get => (TimeSpan?)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } [Bindable(true), Category("Common")] public TimeSpan? Minimum { get => (TimeSpan?)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } [Bindable(true), Category("Common")] public bool AvoidScroll { get => (bool)GetValue(AvoidScrollProperty); set => SetValue(AvoidScrollProperty, value); } [Bindable(true), Category("Common")] public bool DisplaySeconds { get => (bool)GetValue(DisplaySecondsProperty); set => SetValue(DisplaySecondsProperty, value); } [Bindable(true), Category("Common")] public bool DisplayMilliseconds { get => (bool)GetValue(DisplayMillisecondsProperty); set => SetValue(DisplayMillisecondsProperty, value); } [Bindable(true), Category("Common")] public bool DisplayEmptyAsMidnight { get => (bool)GetValue(DisplayEmptyAsMidnightProperty); set => SetValue(DisplayEmptyAsMidnightProperty, value); } protected string Format => "hh':'mm" + (DisplaySeconds ? "':'ss" + (DisplayMilliseconds ? "'.'fff" : "") : ""); #endregion #region Property Changed private static void Selected_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is TimeBox timeBox) || timeBox._ignore) return; if (timeBox.Selected > timeBox.Maximum) { timeBox.Tag = timeBox.Maximum; timeBox.Selected = timeBox.Maximum; } else if (timeBox.Selected < timeBox.Minimum) { timeBox.Tag = timeBox.Minimum; timeBox.Selected = timeBox.Minimum; } timeBox.Text = timeBox.Selected?.ToString(timeBox.Format, CultureInfo.InvariantCulture) ?? ""; } private static void Maximum_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var timeBox = d as TimeBox; if (!(timeBox?.Tag is TimeSpan selected)) return; if (selected > timeBox.Maximum) { timeBox.Tag = timeBox.Maximum; timeBox.Selected = timeBox.Maximum; } } private static void Minimum_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var timeBox = d as TimeBox; if (!(timeBox?.Tag is TimeSpan selected)) return; if (selected < timeBox.Minimum) { timeBox.Tag = timeBox.Minimum; timeBox.Selected = timeBox.Minimum; } } #endregion static TimeBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(TimeBox), new FrameworkPropertyMetadata(typeof(TimeBox))); } #region Overrides protected override void OnPreviewTextInput(TextCompositionEventArgs e) { base.OnPreviewTextInput(e); if (_ignore) return; if (SelectionLength > 0) { e.Handled = false; return; } if (Text.Length + e.Text.Length < 2) return; #region Hour 01 if (Text.Length == 1) { //Text property is old. In order to test, add new characters right now. Text += e.TextComposition.Text; //Validate if it's a valid hour value (0 - 23). if (int.TryParse(Text.Substring(0, 2), out var hour)) { if (hour > 23) Text = "23"; Select(Text.Length, 0); e.Handled = true; } return; } #endregion #region Minute 01:02 if (Text.Length == 4) { Text += e.TextComposition.Text; //Validate if it's a valid minute value (0 - 59). if (int.TryParse(Text.Substring(3, 2), out var minute)) { if (minute > 59) Text = Text.Substring(0, 3) + "59"; Select(Text.Length, 0); e.Handled = true; } return; } #endregion if (!DisplaySeconds && Text.Length > 4) { UpdateSource(); if (!e.Handled) MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); e.Handled = true; return; } #region Second 01:02:03 if (Text.Length == 7) { Text = Text.Insert(SelectionStart, e.TextComposition.Text); //Validate if it's a valid seconds value (0 to 59). if (int.TryParse(Text.Substring(6, 2), out var second)) { if (second > 59) Text = Text.Substring(0, 6) + "59"; e.Handled = true; } } #endregion #region Millisecond 01:02:03.004 if (Text.Length == 11) { Text = Text.Insert(SelectionStart, e.TextComposition.Text); //SelectionStart = 7; //SelectionLength = 0; } #endregion //Don't let the user add more numbers if the maximum length will be surpassed. if (Text.Length > (DisplayMilliseconds ? 11 : 6)) { UpdateSource(); if (!e.Handled) MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); e.Handled = true; } } protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnPreviewKeyDown(e); #region Navigation or selection if (e.Key == Key.Enter || e.Key == Key.Return || e.Key == Key.Tab || e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Escape || e.Key == Key.Home || e.Key == Key.End) { e.Handled = false; return; } #endregion if (IsReadOnly) { e.Handled = true; return; } #region Remove if (e.Key == Key.Back || e.Key == Key.Delete) { if (SelectionLength == Text.Length || Text.Length == 1 && (SelectionStart == 0 && e.Key == Key.Delete || SelectionStart == 1 && e.Key == Key.Back)) { Text = ""; RaiseEvent(new RoutedEventArgs(TextChangedEvent)); UpdateSource(); } e.Handled = false; return; } #endregion #region Colon (:) and period (.) if ((e.Key == Key.OemQuestion || e.Key == Key.OemPeriod) && (Keyboard.Modifiers & ModifierKeys.Control) == 0) { var separatorSelected = Text.Substring(SelectionStart, SelectionLength).Contains(":") || Text.Substring(SelectionStart, SelectionLength).Contains("."); //Let it add a separator if in the right position. if (SelectionStart == 2 || SelectionStart == 5 && DisplaySeconds || SelectionStart == 8 || separatorSelected) { e.Handled = false; return; } if (Text.Length > 8) { e.Handled = true; return; } #region Adds the hour, minute, second and millisecond //1 --> 01: //0 --> 01: if (Text.Length == 1) Text = "0" + (Text.Equals("0") ? "1" : Text) + ":"; //01:2 --> 01:02: //01:0 --> 01:01: else if (Text.Length == 4) Text = Text.Substring(0, 3) + "0" + (Text.Substring(3, 1).Equals("0") ? "1" : Text.Substring(3, 1)) + (DisplaySeconds ? ":" : ""); //01:02:5 --> 01:02:05 //01:02:0 --> 01:02:00 else if (Text.Length == 7) Text = Text.Substring(0, 6) + "0" + Text.Substring(6, 1); //01:02:03.5 --> 01:02:03.005 //01:02:03.0 --> 01:02:03.000 else if (Text.Length == 10) Text = Text.Substring(0, 9) + Text.Substring(6, 1).PadLeft(3, '0'); #endregion SelectionStart = Text.Length; e.Handled = true; return; } #endregion #region Numeric if (e.Key >= Key.D0 && e.Key <= Key.D9 || e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9) { //01 if (Text.Length - SelectionLength == 2) { Text = Text + ":"; Select(Text.Length, 0); } //01:02 if (Text.Length - SelectionLength == 5 && DisplaySeconds) { Text = Text + ":"; Select(Text.Length, 0); } //01:02:03 if (Text.Length - SelectionLength == 8 && DisplayMilliseconds) { Text = Text + "."; Select(Text.Length, 0); } e.Handled = false; return; } #endregion #region Value Navigation if (Keyboard.Modifiers == ModifierKeys.Control) { //System's actions. Ignore. if (e.Key == Key.A || e.Key == Key.X || e.Key == Key.C || e.Key == Key.V) { e.Handled = false; return; } //Now or maximum. if (e.Key == Key.OemSemicolon || e.Key == Key.Oem2) { //Text = DateTime.Now.TimeOfDay.ToString(Format); Selected = Maximum ?? DateTime.Now.TimeOfDay; SelectAll(); return; } //Increase or decrease. if (e.Key == Key.OemComma || e.Key == Key.Decimal) { Change(Selected, -1, TimeSpan.FromMinutes(1)); //Text = string.IsNullOrWhiteSpace(Text) ? DateTime.Now.TimeOfDay.ToString(Format) : Text; ////Previous minute. //if (TimeSpan.TryParse(Text, out var aux)) //{ // if (aux - TimeSpan.FromMinutes(1) < (Minimum ?? TimeSpan.Zero)) //Deal with milliseconds... // { // aux = Maximum ?? new TimeSpan(0, 23, 59, 59, 999); // Text = aux.ToString(Format); // } // else // Text = aux.Add(TimeSpan.FromMinutes(-1)).ToString(Format); //} } else if (e.Key == Key.OemPeriod) { Change(Selected, 1, TimeSpan.FromMinutes(1)); //Text = string.IsNullOrWhiteSpace(Text) ? DateTime.Now.TimeOfDay.ToString(Format) : Text; ////Next minute. //if (TimeSpan.TryParse(Text, out var aux)) //{ // if (aux + TimeSpan.FromMinutes(1) > (Maximum ?? new TimeSpan(0, 23, 59, 59, 999))) //Deal with milliseconds... // { // aux = Minimum ?? TimeSpan.Zero; // Text = aux.ToString(Format); // } // else // Text = aux.Add(TimeSpan.FromMinutes(1)).ToString(Format); //} } //UpdateSource(); } #endregion } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); SelectAll(); } protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e) { if (e.NewFocus == e.OldFocus) return; //Validate on LostFocus. if (!TimeSpan.TryParse(Text, out var aux)) { Selected = null; } else { //If the TryParse converted a single digit group to days, transform it to hours. if (aux.Days > 0 && aux.Days < 24 && aux.Minutes == 0 && aux.Seconds == 0) aux = new TimeSpan(aux.Days, 0, 0); Selected = aux; } UpdateSource(); base.OnPreviewLostKeyboardFocus(e); } protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) { //Validate on LostFocus. if (!TimeSpan.TryParse(Text, out var aux)) { Selected = null; } else { //If the TryParse converted a single digit group to days, transform it to hours. if (aux.Days > 0 && aux.Days < 24 && aux.Minutes == 0 && aux.Seconds == 0) aux = new TimeSpan(aux.Days, 0, 0); Selected = aux; } UpdateSource(); base.OnLostKeyboardFocus(e); } protected override void OnLostFocus(RoutedEventArgs e) { //Validate on LostFocus. if (!TimeSpan.TryParse(Text, out var aux)) { Selected = null; } else { //If the TryParse converted a single digit group to days, transform it to hours. if (aux.Days > 0 && aux.Days < 24 && aux.Minutes == 0 && aux.Seconds == 0) aux = new TimeSpan(aux.Days, 0, 0); Selected = aux; } UpdateSource(); base.OnLostFocus(e); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { if (!IsKeyboardFocusWithin) { e.Handled = true; Focus(); } } protected override void OnMouseWheel(MouseWheelEventArgs e) { if (IsReadOnly || AvoidScroll || !IsFocused) { base.OnMouseWheel(e); return; } switch (Keyboard.Modifiers) { case ModifierKeys.Control: //Milliseconds. { if (!DisplayMilliseconds) return; Selected = Change(Selected, e.Delta, new TimeSpan(0, 0, 0, 0, 100)); break; } case ModifierKeys.None: //Seconds. { if (!DisplaySeconds) return; Selected = Change(Selected, e.Delta, new TimeSpan(0, 0, 1)); break; } case ModifierKeys.Shift: //Minutes. { Selected = Change(Selected, e.Delta, new TimeSpan(0, 1, 0)); break; } case ModifierKeys.Shift | ModifierKeys.Control: //Hours. { Selected = Change(Selected, e.Delta, new TimeSpan(1, 0, 0)); break; } } e.Handled = true; base.OnMouseWheel(e); } #endregion #region Methods private void UpdateSource() { var prop = GetBindingExpression(TextProperty); prop?.UpdateSource(); } private TimeSpan Change(TimeSpan? current, int delta, TimeSpan amount) { return delta > 0 ? current?.Add(amount) ?? Maximum ?? new TimeSpan(0, 23, 59, 59, 999) : current?.Subtract(amount) ?? Minimum ?? new TimeSpan(0, 0, 0); } #endregion } ================================================ FILE: ScreenToGif/Controls/WebcamControl.xaml ================================================  ================================================ FILE: ScreenToGif/Controls/WebcamControl.xaml.cs ================================================ using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using ScreenToGif.Util; using ScreenToGif.Webcam.DirectX; using ScreenToGif.Windows.Other; namespace ScreenToGif.Controls; public partial class WebcamControl : UserControl { #region Variables public CaptureWebcam Capture { get; set; } public Filter VideoDevice { get; set; } #endregion #region Properties public int VideoWidth => Capture?.Width ?? -1; public int VideoHeight => Capture?.Height ?? -1; #endregion public WebcamControl() { InitializeComponent(); } #region Private Methods private bool IsInDesignMode() { return DesignerProperties.GetIsInDesignMode(this); } private double Scale() { var source = PresentationSource.FromVisual(this); if (source?.CompositionTarget != null) return source.CompositionTarget.TransformToDevice.M11; return 1d; } #endregion #region Public Methods public void Refresh() { try { //To change the video device, a dispose is needed. if (Capture != null) { Capture.Dispose(); Capture = null; } //Create capture object. if (VideoDevice != null) { Capture = new CaptureWebcam(VideoDevice) { PreviewWindow = this, Scale = Scale() }; Capture.StartPreview(); //Width = Height * ((double)Capture.Width / (double)Capture.Height); } } catch (Exception e) { LogWriter.Log(e, "It was not possible to access the webcam feed."); ErrorDialog.Ok("ScreenToGif", "It was not possible to access the webcam's feed", e.Message, e); } } public void Unload() { if (Capture != null) { Capture.StopPreview(); Capture.Dispose(); } VideoDevice = null; GC.Collect(); } #endregion #region Events private void WebcamControl_OnLoaded(object sender, RoutedEventArgs e) { //Don't show the feed if in design mode. if (IsInDesignMode()) return; Refresh(); } private void UserControl_Unloaded(object sender, RoutedEventArgs e) { Unload(); } #endregion } ================================================ FILE: ScreenToGif/Controls/ZoomBox.cs ================================================ using System; using System.ComponentModel; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using ScreenToGif.Util; using ScreenToGif.Util.Extensions; namespace ScreenToGif.Controls; /// /// A zoomable control. /// http://www.codeproject.com/Articles/97871/WPF-simple-zoom-and-drag-support-in-a-ScrollViewer /// http://www.codeproject.com/Articles/85603/A-WPF-custom-control-for-zooming-and-panning /// [TemplatePart(Name = "ScrollViewer", Type = typeof(ScrollViewer))] public class ZoomBox : Control { #region Variables private Point? _lastCenterPositionOnTarget; private Point? _lastMousePositionOnTarget; private Point? _lastDragPoint; private ScrollViewer _scrollViewer; private ScaleTransform _scaleTransform; private Grid _grid; private double _previousZoom = 1d; #endregion #region Dependency Properties public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(string), typeof(ZoomBox), new FrameworkPropertyMetadata(ImageSource_PropertyChanged)); public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register("Zoom", typeof(double), typeof(ZoomBox), new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsRender, Zoom_PropertyChanged)); public static readonly DependencyProperty ImageScaleProperty = DependencyProperty.Register("ImageScale", typeof(double), typeof(ZoomBox), new FrameworkPropertyMetadata(0.1, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty PixelSizeProperty = DependencyProperty.Register("PixelSize", typeof(Size), typeof(ZoomBox), new FrameworkPropertyMetadata(new Size(0, 0), FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty FitImageProperty = DependencyProperty.Register("FitImage", typeof(bool), typeof(ZoomBox), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender)); #endregion #region Properties /// /// The image source. /// [Description("The image source.")] public string ImageSource { get => (string)GetValue(ImageSourceProperty); set => SetValue(ImageSourceProperty, value); } /// /// The zoom level of the control. /// [Description("The zoom level of the control.")] public double Zoom { get => (double)GetValue(ZoomProperty); set => SetCurrentValue(ZoomProperty, value); } /// /// The scale (dpi/96) of the screen. /// [Description("The zoom level of the control.")] public double ImageScale { get => (double)GetValue(ImageScaleProperty); set => SetCurrentValue(ImageScaleProperty, value); } /// /// The pixel size of the image, independently of DPI. /// [Description("The pixel size of the image, independently of DPI.")] public Size PixelSize { get => (Size)GetValue(PixelSizeProperty); set => SetCurrentValue(PixelSizeProperty, value); } /// /// Decides if it should fit the image on start. /// [Description("Decides if it should fit the image on start.")] public bool FitImage { get => (bool)GetValue(FitImageProperty); set => SetCurrentValue(FitImageProperty, value); } /// /// The DPI of the image. /// public double ImageDpi { get; set; } /// /// The amount of scale of the image x the visuals. /// (Dpi of the images compared with the dpi of the UIElements). /// public double ScaleDiff { get; set; } #endregion #region Custom Events /// /// Create a custom routed event by first registering a RoutedEventID, this event uses the bubbling routing strategy. /// public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ZoomBox)); /// /// Event raised when the numeric value is changed. /// public event RoutedEventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } public void RaiseValueChangedEvent() { if (ValueChangedEvent == null || !IsLoaded) return; var newEventArgs = new RoutedEventArgs(ValueChangedEvent); RaiseEvent(newEventArgs); } #endregion static ZoomBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomBox), new FrameworkPropertyMetadata(typeof(ZoomBox))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); _scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer; _scaleTransform = GetTemplateChild("ScaleTransform") as ScaleTransform; _grid = GetTemplateChild("Grid") as Grid; if (_scrollViewer != null) { _scrollViewer.ScrollChanged += OnScrollViewerScrollChanged; _scrollViewer.MouseLeftButtonUp += OnMouseLeftButtonUp; _scrollViewer.PreviewMouseLeftButtonUp += OnMouseLeftButtonUp; _scrollViewer.PreviewMouseRightButtonUp += OnPreviewMouseRightButtonUp; _scrollViewer.PreviewMouseWheel += OnPreviewMouseWheel; _scrollViewer.PreviewMouseLeftButtonDown += OnMouseLeftButtonDown; _scrollViewer.MouseMove += OnMouseMove; } } #region Events private static void ImageSource_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is ZoomBox zoomBox)) return; zoomBox.ImageSource = e.NewValue as string; } private static void Zoom_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is ZoomBox box)) return; if (!(e.NewValue is double value)) return; //Maximum and minimum. if (value < 0.1) box.Zoom = 0.1; if (value > 5.0) box.Zoom = 5; box.RefreshImage(); } private static void ImageScale_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is ZoomBox box)) return; box.RefreshImage(); } private void OnPreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) { if (Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl)) Reset(); } private void OnMouseMove(object sender, MouseEventArgs e) { if (!_lastDragPoint.HasValue) return; var posNow = e.GetPosition(_scrollViewer); var dX = posNow.X - _lastDragPoint.Value.X; var dY = posNow.Y - _lastDragPoint.Value.Y; _lastDragPoint = posNow; _scrollViewer.ScrollToHorizontalOffset(_scrollViewer.HorizontalOffset - dX); _scrollViewer.ScrollToVerticalOffset(_scrollViewer.VerticalOffset - dY); } private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var mousePos = e.GetPosition(_scrollViewer); if (mousePos.X <= _scrollViewer.ViewportWidth && mousePos.Y < _scrollViewer.ViewportHeight) //make sure we still can use the scrollbars { _scrollViewer.Cursor = Cursors.Hand; _lastDragPoint = mousePos; Mouse.Capture(_scrollViewer); } } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _scrollViewer.Cursor = Cursors.Arrow; _scrollViewer.ReleaseMouseCapture(); _lastDragPoint = null; } private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { _lastMousePositionOnTarget = e.GetPosition(_grid); switch (Keyboard.Modifiers) { case ModifierKeys.Control: #region Zoom if (e.Delta > 0) { if (Zoom < 5.0) Zoom += 0.1; } if (e.Delta < 0) { if (Zoom > 0.2) Zoom -= 0.1; } var centerOfViewport = new Point(_scrollViewer.ViewportWidth / 2, _scrollViewer.ViewportHeight / 2); _lastCenterPositionOnTarget = _scrollViewer.TranslatePoint(centerOfViewport, _grid); #endregion break; case ModifierKeys.Alt: var verDelta = e.Delta > 0 ? -10.5 : 10.5; _scrollViewer.ScrollToVerticalOffset(_scrollViewer.VerticalOffset + verDelta); break; case ModifierKeys.Shift: var horDelta = e.Delta > 0 ? -10.5 : 10.5; _scrollViewer.ScrollToHorizontalOffset(_scrollViewer.HorizontalOffset + horDelta); break; } e.Handled = true; } private void OnScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e) { if (Math.Abs(e.ExtentHeightChange) < 0.01 && Math.Abs(e.ExtentWidthChange) < 0.01) return; Point? targetBefore = null; Point? targetNow = null; if (!_lastMousePositionOnTarget.HasValue) { if (_lastCenterPositionOnTarget.HasValue) { var centerOfViewport = new Point(_scrollViewer.ViewportWidth / 2, _scrollViewer.ViewportHeight / 2); var centerOfTargetNow = _scrollViewer.TranslatePoint(centerOfViewport, _grid); targetBefore = _lastCenterPositionOnTarget; targetNow = centerOfTargetNow; } } else { targetBefore = _lastMousePositionOnTarget; targetNow = Mouse.GetPosition(_grid); _lastMousePositionOnTarget = null; } if (!targetBefore.HasValue) return; var dXInTargetPixels = targetNow.Value.X - targetBefore.Value.X; var dYInTargetPixels = targetNow.Value.Y - targetBefore.Value.Y; var multiplicatorX = e.ExtentWidth / _grid.ActualWidth; var multiplicatorY = e.ExtentHeight / _grid.ActualHeight; var newOffsetX = _scrollViewer.HorizontalOffset - dXInTargetPixels * multiplicatorX; var newOffsetY = _scrollViewer.VerticalOffset - dYInTargetPixels * multiplicatorY; if (double.IsNaN(newOffsetX) || double.IsNaN(newOffsetY)) return; _scrollViewer.ScrollToHorizontalOffset(newOffsetX); _scrollViewer.ScrollToVerticalOffset(newOffsetY); } #endregion public void LoadFromPath(string path) { ImageSource = path; using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { var bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnDemand; bitmapImage.StreamSource = stream; bitmapImage.EndInit(); PixelSize = new Size(bitmapImage.PixelWidth, bitmapImage.PixelHeight); ImageScale = Math.Round(bitmapImage.DpiX / 96d, 2); } RefreshImage(); } public void RefreshImage() { //ImageScale = ImageSource.ScaleOf(); //Calculates how much bigger or smaller the image should be presented, based on the window and image scale (DPI/96). ImageDpi = ImageScale * 96d; ScaleDiff = this.Scale() / ImageScale; //Apply the zoom, with the scale difference. if (_scaleTransform != null) { _scaleTransform.ScaleX = Zoom / ScaleDiff; _scaleTransform.ScaleY = Zoom / ScaleDiff; } //Raise event. RaiseValueChangedEvent(); } /// /// Resets the Scale and Position of the Child element. /// public void Reset() { //Resets the zoom. Zoom = 1; //Resets the position. //var tt = GetTranslateTransform(_child); //tt.X = 0.0; //tt.Y = 0.0; } /// /// Save the current zoom level. /// public void SaveCurrentZoom() { _previousZoom = Zoom; } /// /// Returns to the previously saved zoom level. /// public void RestoreSavedZoom() { //Resets the zoom. Zoom = _previousZoom; } /// /// Removes the image. /// public void Clear() { ImageSource = null; GC.Collect(1); } /// /// Gets the ScrollViewer. /// /// A ScrollViewer. public ScrollViewer GetScrollViewer() { return _scrollViewer; } /// /// Gets how the element is displayed, base on current screen DPI versus image DPI. /// /// The actual size * the scale of the element. public Size GetElementSize(bool noScalling = false) { if (_scrollViewer.Content is not FrameworkElement image) return new Size(Math.Max(ActualWidth, 0), Math.Max(ActualHeight, 0)); var scaleX = noScalling ? 1 : _scaleTransform.ScaleX; var scaleY = noScalling ? 1 : _scaleTransform.ScaleY; return new Size(image.ActualWidth * scaleX, image.ActualHeight * scaleY); } /// /// Gets the actual image size. /// /// The actual image size. public Size GetImageSize() { if (_scrollViewer.Content is not FrameworkElement image) return new Size(Math.Max(ActualWidth, 0), Math.Max(ActualHeight, 0)); //Ignore scale transform? return new Size(image.ActualWidth * ImageScale, image.ActualHeight * ImageScale); } public Size MeasureImageSizeAtZoom100(string path) { var image = path.SourceFrom(); var imageScale = Math.Round(image.DpiX / 96d, 2); var scaleDiff = this.Scale() / imageScale; //var size = new Size(image.Width * imageScale, image.Height * imageScale); return new Size(image.Width * 1d / scaleDiff, image.Height * 1d / scaleDiff); } } ================================================ FILE: ScreenToGif/Docs/Documentation.md ================================================ ## ScreenToGif Developer Documentation Would you like to help build this developer documentation? ### Sections From an user perspective, the app is divided into two main parts, the recorders and the editor. ================================================ FILE: ScreenToGif/ImageUtil/ImageMethods.cs ================================================ using ScreenToGif.Domain.Enums; using ScreenToGif.Domain.Interfaces; using ScreenToGif.Domain.Models; using ScreenToGif.Util; using ScreenToGif.Util.Codification; using ScreenToGif.Util.Codification.Gif.Decoder; using ScreenToGif.Util.Codification.Gif.LegacyEncoder; using ScreenToGif.Util.Extensions; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Resources; using Image = System.Drawing.Image; using PixelFormat = System.Windows.Media.PixelFormat; using Size = System.Drawing.Size; using Color = System.Windows.Media.Color; namespace ScreenToGif.ImageUtil; /// /// Image algorithms. /// public static class ImageMethods { #region Gif transparency /// /// Gets the project, scans the each image in the list, replacing the color with a color that will be treated as transparent by the encoder. /// /// The exported project. /// The color that will be converted to the chroma key, which in turn will be treated as transparent. If null, takes all colors with transparency and convert to the chroma. /// The color that will be treated as transparent. /// The id of the encoding task. /// The cancellation token source. /// The export project, with the images already scanned and altered. public static ExportProject PaintAndCutForTransparency(ExportProject project, System.Windows.Media.Color? source, System.Windows.Media.Color chroma, int taskId, CancellationTokenSource tokenSource) { using (var oldStream = new FileStream(project.ChunkPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var newStream = new FileStream(project.NewChunkPath, FileMode.Create, FileAccess.Write, FileShare.None)) { for (var index = 0; index < project.Frames.Count; index++) { #region Cancellation if (tokenSource.Token.IsCancellationRequested) { EncodingManager.Update(taskId, EncodingStatus.Canceled); break; } #endregion #region For each frame EncodingManager.Update(taskId, index); //var watch = Stopwatch.StartNew(); #region Get image info oldStream.Position = project.Frames[index].DataPosition; var pixels = oldStream.ReadBytes((int)project.Frames[index].DataLength); var startY = new bool[project.Frames[index].Rect.Height]; var startX = new bool[project.Frames[index].Rect.Width]; var height = project.Frames[index].Rect.Height; var width = project.Frames[index].Rect.Width; var blockCount = project.Frames[index].ImageDepth / 8; #endregion //Console.WriteLine("Info: " + watch.Elapsed); //Only use Parallel if the image is big enough. if (width * height > 150000) { #region Parallel loop //x - width - sides Parallel.For(0, pixels.Length / blockCount, i => { i *= blockCount; //Replace all transparent color to a transparent version of the chroma key. //Replace all colors that match the source color with a transparent version of the chroma key. if ((!source.HasValue && pixels[i + 3] == 0) || (source.HasValue && pixels[i] == source.Value.B && pixels[i + 1] == source.Value.G && pixels[i + 2] == source.Value.R)) { pixels[i] = chroma.B; pixels[i + 1] = chroma.G; pixels[i + 2] = chroma.R; pixels[i + 3] = 0; } else { var y = i / blockCount / width; var x = i / blockCount - (y * width); //var current = (y * image1.Width + x) * blockCount == i; startX[x] = true; startY[y] = true; } }); #endregion } else { #region Sequential loop for (var i = 0; i < pixels.Length; i += blockCount) { //Replace all transparent color to a transparent version of the chroma key. //Replace all colors that match the source color with a transparent version of the chroma key. if ((!source.HasValue && pixels[i + 3] == 0) || (source.HasValue && pixels[i] == source.Value.B && pixels[i + 1] == source.Value.G && pixels[i + 2] == source.Value.R)) { pixels[i] = chroma.B; pixels[i + 1] = chroma.G; pixels[i + 2] = chroma.R; pixels[i + 3] = 0; } else { //Actual content, that should be ignored. var y = i / blockCount / width; var x = i / blockCount - (y * width); //var current = (y * image1.Width + x) * blockCount == i; startX[x] = true; startY[y] = true; } } #endregion } //Console.WriteLine("Change: " + watch.Elapsed); //First frame gets ignored. if (index == 0) { project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = pixels.LongLength; newStream.WriteBytes(pixels); continue; } #region Verify positions var firstX = startX.ToList().FindIndex(x => x); var lastX = startX.ToList().FindLastIndex(x => x); if (firstX == -1) firstX = 0; if (lastX == -1) lastX = width; var firstY = startY.ToList().FindIndex(x => x); var lastY = startY.ToList().FindLastIndex(x => x); if (lastY == -1) lastY = height; if (firstY == -1) firstY = 0; if (lastX < firstX) { var aux = lastX; lastX = firstX; firstX = aux; } if (lastY < firstY) { var aux = lastY; lastY = firstY; firstY = aux; } #endregion #region Get the Width and Height var heightCut = Math.Abs(lastY - firstY); var widthCut = Math.Abs(lastX - firstX); //If nothing changed, shift the delay. if (heightCut + widthCut == height + width) { //TODO: Maximum of 2 bytes, 255 x 100: 25.500 ms project.Frames[index].Rect = new Int32Rect(0, 0, 0, 0); project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = 0; GC.Collect(1); continue; } if (heightCut != height) heightCut++; if (widthCut != width) widthCut++; project.Frames[index].Rect = new Int32Rect(firstX, firstY, widthCut, heightCut); #endregion #region Crop and save var newPixels = CropImageArray(pixels, width, 32, project.Frames[index].Rect); project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = newPixels.LongLength; newStream.WriteBytes(newPixels); #endregion //Console.WriteLine("Save: " + watch.Elapsed); //Console.WriteLine(); GC.Collect(1); #endregion } } } EncodingManager.Update(taskId, LocalizationHelper.Get("S.Encoder.SavingAnalysis"), true); //Detect any empty frame. for (var index = project.Frames.Count - 1; index >= 0; index--) { if (project.Frames[index].DataLength == 0) project.Frames[index - 1].Delay += project.Frames[index].Delay; } //Replaces the chunk file. File.Delete(project.ChunkPath); File.Move(project.NewChunkPath, project.ChunkPath); return project; } /// /// Analyzes all frames (from the end to the start) and paints all unchanged pixels with a given color, /// after, it cuts the image to reduce filesize. /// /// The project with frames to analyze. /// The color to paint the unchanged pixels. /// The Id of the current Task. /// The cancellation token source. /// The project containing all frames and its cut points. public static ExportProject PaintTransparentAndCut(ExportProject project, System.Windows.Media.Color chroma, int taskId, CancellationTokenSource tokenSource) { using (var oldStream = new FileStream(project.ChunkPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var newFileStream = new FileStream(project.NewChunkPath, FileMode.Create, FileAccess.Write, FileShare.None)) { using (var newStream = new BufferedStream(newFileStream, 100 * 1048576)) //Each 1 MB has 1_048_576 bytes. { for (var index = project.Frames.Count - 1; index > 0; index--) { #region Cancellation if (tokenSource.Token.IsCancellationRequested) { EncodingManager.Update(taskId, EncodingStatus.Canceled); break; } #endregion #region For each frame, from the end to the start EncodingManager.Update(taskId, project.Frames.Count - index - 1); //var watch = Stopwatch.StartNew(); #region Get image info oldStream.Position = project.Frames[index - 1].DataPosition; var image1 = oldStream.ReadBytes((int)project.Frames[index - 1].DataLength); //Previous image. oldStream.Position = project.Frames[index].DataPosition; var image2 = oldStream.ReadBytes((int)project.Frames[index].DataLength); //Current image. var startY = new bool[project.Frames[index - 1].Rect.Height]; var startX = new bool[project.Frames[index - 1].Rect.Width]; var height = project.Frames[index - 1].Rect.Height; var width = project.Frames[index - 1].Rect.Width; var blockCount = project.Frames[index - 1].ImageDepth / 8; #endregion //Console.WriteLine("Info: " + watch.Elapsed); //Only use Parallel if the image is big enough. if (width * height > 150000) { #region Parallel Loop //x - width - sides Parallel.For(0, image1.Length / blockCount, i => { i *= blockCount; if (image1[i] != image2[i] || image1[i + 1] != image2[i + 1] || image1[i + 2] != image2[i + 2]) { //Different pixels should remain. var y = i / blockCount / width; var x = i / blockCount - (y * width); //image2[i + 3] = 255; When saving frames with transparency without the 'Enable transparency' ticked, the pixels that changed should be set to opaque. startX[x] = true; startY[y] = true; } else { image2[i] = chroma.B; image2[i + 1] = chroma.G; image2[i + 2] = chroma.R; image2[i + 3] = 0; } }); #endregion } else { #region Sequential loop for (var i = 0; i < image1.Length; i += blockCount) { if (image1[i] != image2[i] || image1[i + 1] != image2[i + 1] || image1[i + 2] != image2[i + 2]) { //Different pixels should remain. var y = i / blockCount / width; var x = i / blockCount - (y * width); //image2[i + 3] = 255; When saving frames with transparency without the 'Enable transparency' ticked, the pixels that changed should be set to opaque. startX[x] = true; startY[y] = true; } else { image2[i] = chroma.B; image2[i + 1] = chroma.G; image2[i + 2] = chroma.R; image2[i + 3] = 0; } } #endregion } //Console.WriteLine("Change: " + watch.Elapsed); #region Verify positions var firstX = startX.ToList().FindIndex(x => x); var lastX = startX.ToList().FindLastIndex(x => x); if (firstX == -1) firstX = 0; if (lastX == -1) lastX = width; var firstY = startY.ToList().FindIndex(x => x); var lastY = startY.ToList().FindLastIndex(x => x); if (lastY == -1) lastY = height; if (firstY == -1) firstY = 0; if (lastX < firstX) { var aux = lastX; lastX = firstX; firstX = aux; } if (lastY < firstY) { var aux = lastY; lastY = firstY; firstY = aux; } #endregion #region Get the Width and Height var heightCut = Math.Abs(lastY - firstY); var widthCut = Math.Abs(lastX - firstX); //If nothing changed, shift the delay. if (heightCut + widthCut == height + width) { //TODO: Maximum of 2 bytes, 255 x 100: 25.500 ms project.Frames[index - 1].Delay += project.Frames[index].Delay; project.Frames[index].Rect = new Int32Rect(0, 0, 0, 0); project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = 0; GC.Collect(1); continue; } if (heightCut != height) heightCut++; if (widthCut != width) widthCut++; project.Frames[index].Rect = new Int32Rect(firstX, firstY, widthCut, heightCut); #endregion #region Crop and save var newPixels = CropImageArray(image2, width, 32, project.Frames[index].Rect); //Writes to the buffer from end to start. Since I have the position, it does not matter. project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = newPixels.LongLength; newStream.WriteBytes(newPixels); #endregion //SavePixelArrayToFile(newPixels, project.Frames[index].Rect.Width, project.Frames[index].Rect.Height, 4, project.ChunkPath + index + ".png"); //Console.WriteLine("Save: " + watch.Elapsed); //Console.WriteLine(); GC.Collect(1); #endregion } EncodingManager.Update(taskId, LocalizationHelper.Get("S.Encoder.SavingAnalysis"), true); #region Write the first frame oldStream.Position = project.Frames[0].DataPosition; var firstFrame = oldStream.ReadBytes((int)project.Frames[0].DataLength); project.Frames[0].DataPosition = newStream.Position; project.Frames[0].DataLength = firstFrame.LongLength; //SavePixelArrayToFile(firstFrame, project.Frames[0].Rect.Width, project.Frames[0].Rect.Height, 4, project.ChunkPath + 0 + ".png"); newStream.WriteBytes(firstFrame); #endregion } } } //Detect the data position of each frame. //for (var index = 1; index < project.Frames.Count - 1; index++) // project.Frames[index].DataPosition = project.Frames[index - 1].DataLength + project.Frames[index - 1].DataPosition; //Replaces the chunk file. File.Delete(project.ChunkPath); File.Move(project.NewChunkPath, project.ChunkPath); return project; } /// /// Analyzes all frames (from the end to the start) and paints all unchanged pixels with a given color, /// after, it cuts the image to reduce filesize. /// /// The project with frames to analyze. /// The Id of the Task. /// The cancellation token source. /// The project containing all frames and its cut points. public static ExportProject CutUnchanged(ExportProject project, int taskId, CancellationTokenSource tokenSource) { using (var oldStream = new FileStream(project.ChunkPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var newStream = new FileStream(project.NewChunkPath, FileMode.Create, FileAccess.Write, FileShare.None)) { for (var index = project.Frames.Count - 1; index > 0; index--) { #region Cancellation if (tokenSource.Token.IsCancellationRequested) { EncodingManager.Update(taskId, EncodingStatus.Canceled); break; } #endregion #region For each frame, from the end to the start EncodingManager.Update(taskId, project.Frames.Count - index - 1); //var watch = Stopwatch.StartNew(); #region Get image info oldStream.Position = project.Frames[index - 1].DataPosition; var image1 = oldStream.ReadBytes((int)project.Frames[index - 1].DataLength); //Previous image. oldStream.Position = project.Frames[index].DataPosition; var image2 = oldStream.ReadBytes((int)project.Frames[index].DataLength); //Current image. var startY = new bool[project.Frames[index - 1].Rect.Height]; var startX = new bool[project.Frames[index - 1].Rect.Width]; var height = project.Frames[index - 1].Rect.Height; var width = project.Frames[index - 1].Rect.Width; var blockCount = project.Frames[index - 1].ImageDepth / 8; #endregion //Console.WriteLine("Info: " + watch.Elapsed); //Only use Parallel if the image is big enough. if (width * height > 150000) { #region Parallel Loop //x - width - sides Parallel.For(0, image1.Length / blockCount, i => { i *= blockCount; if (image1[i] != image2[i] || image1[i + 1] != image2[i + 1] || image1[i + 2] != image2[i + 2]) { //Different pixels should remain. var y = i / blockCount / width; var x = i / blockCount - (y * width); //var current = (y * image1.Width + x) * blockCount == i; startX[x] = true; startY[y] = true; } }); #endregion } else { #region Sequential loop for (var i = 0; i < image1.Length; i += blockCount) { if (image1[i] != image2[i] || image1[i + 1] != image2[i + 1] || image1[i + 2] != image2[i + 2]) { //Different pixels should remain. var y = i / blockCount / width; var x = i / blockCount - (y * width); //var current = (y * image1.Width + x) * blockCount == i; startX[x] = true; startY[y] = true; } } #endregion } //Console.WriteLine("Change: " + watch.Elapsed); #region Verify positions var firstX = startX.ToList().FindIndex(x => x); var lastX = startX.ToList().FindLastIndex(x => x); if (firstX == -1) firstX = 0; if (lastX == -1) lastX = width; var firstY = startY.ToList().FindIndex(x => x); var lastY = startY.ToList().FindLastIndex(x => x); if (lastY == -1) lastY = height; if (firstY == -1) firstY = 0; if (lastX < firstX) { var aux = lastX; lastX = firstX; firstX = aux; } if (lastY < firstY) { var aux = lastY; lastY = firstY; firstY = aux; } #endregion #region Get the Width and Height var heightCut = Math.Abs(lastY - firstY); var widthCut = Math.Abs(lastX - firstX); //If nothing changed, shift the delay. if (heightCut + widthCut == height + width) { //TODO: Maximum of 2 bytes, 255 x 100: 25.500 ms project.Frames[index - 1].Delay += project.Frames[index].Delay; project.Frames[index].Rect = new Int32Rect(0, 0, 0, 0); project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = 0; GC.Collect(1); continue; } if (heightCut != height) heightCut++; if (widthCut != width) widthCut++; project.Frames[index].Rect = new Int32Rect(firstX, firstY, widthCut, heightCut); #endregion #region Crop and save var newPixels = CropImageArray(image2, width, 32, project.Frames[index].Rect); //Writes to the buffer from end to start. Since I have the position, it does not matter. project.Frames[index].DataPosition = newStream.Position; project.Frames[index].DataLength = newPixels.LongLength; newStream.WriteBytes(newPixels); #endregion //Console.WriteLine("Save: " + watch.Elapsed); //Console.WriteLine(); GC.Collect(1); #endregion } EncodingManager.Update(taskId, LocalizationHelper.Get("S.Encoder.SavingAnalysis"), true); #region Write the first frame oldStream.Position = project.Frames[0].DataPosition; var firstFrame = oldStream.ReadBytes((int)project.Frames[0].DataLength); project.Frames[0].DataPosition = newStream.Position; project.Frames[0].DataLength = firstFrame.LongLength; newStream.WriteBytes(firstFrame); #endregion } } //Detect the data position of each frame. //for (var index = 1; index < project.Frames.Count - 1; index++) // project.Frames[index].DataPosition = project.Frames[index - 1].DataLength + project.Frames[index - 1].DataPosition; //Replaces the chunk file. File.Delete(project.ChunkPath); File.Move(project.NewChunkPath, project.ChunkPath); return project; } public static List