Varias mejoras como auto asignar el foco si la aplaicacion ya esta ejecutandose. Modo transparente. Funcion de Pinned .

This commit is contained in:
Miguel 2025-06-29 14:41:19 +02:00
commit d3dfec8d95
20 changed files with 2218 additions and 0 deletions

418
.gitignore vendored Normal file
View File

@ -0,0 +1,418 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Approval Tests result files
*.received.*
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
**/.fake/
# CodeRush personal settings
**/.cr/personal
# Python Tools for Visual Studio (PTVS)
**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
#tools/**
#!tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
MSBuild_Logs/
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
**/.mfractor/
# Local History for Visual Studio
**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp

26
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net8.0-windows/ShortcutsHelper.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/ShortcutsHelper.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/ShortcutsHelper.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/ShortcutsHelper.sln"
],
"problemMatcher": "$msCompile"
}
]
}

9
App.xaml Normal file
View File

@ -0,0 +1,9 @@
<Application x:Class="ShortcutsHelper.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ShortcutsHelper"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

103
App.xaml.cs Normal file
View File

@ -0,0 +1,103 @@
using System.Configuration;
using System.Data;
using System.Windows;
using System.Threading;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System;
namespace ShortcutsHelper
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : System.Windows.Application
{
private static Mutex? _mutex = null;
private const string MutexName = "ShortcutsHelper_SingleInstance_Mutex";
protected override void OnStartup(StartupEventArgs e)
{
// Intentar crear un mutex para controlar instancia única
bool createdNew;
_mutex = new Mutex(true, MutexName, out createdNew);
if (!createdNew)
{
// Ya hay una instancia ejecutándose
// Intentar activar la ventana existente
ActivateExistingWindow();
// Cerrar esta instancia
Shutdown();
return;
}
// Primera instancia, continuar normalmente
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{
// Liberar el mutex al salir
_mutex?.ReleaseMutex();
_mutex?.Dispose();
base.OnExit(e);
}
private void ActivateExistingWindow()
{
try
{
// Buscar el proceso de ShortcutsHelper que ya está ejecutándose
var currentProcess = Process.GetCurrentProcess();
var processes = Process.GetProcessesByName(currentProcess.ProcessName);
foreach (var process in processes)
{
// Saltar el proceso actual
if (process.Id == currentProcess.Id)
continue;
// Intentar activar la ventana principal del proceso
if (process.MainWindowHandle != IntPtr.Zero)
{
// Si la ventana está minimizada, restaurarla
if (IsIconic(process.MainWindowHandle))
{
ShowWindow(process.MainWindowHandle, SW_RESTORE);
}
// Traer al frente y dar foco
SetForegroundWindow(process.MainWindowHandle);
BringWindowToTop(process.MainWindowHandle);
break;
}
}
}
catch (Exception ex)
{
// Si falla la activación, al menos mostrar un mensaje
System.Windows.MessageBox.Show($"ShortcutsHelper ya está ejecutándose. Error al activar: {ex.Message}",
"Aplicación ya en ejecución",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
}
// APIs de Windows para manejo de ventanas
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
private const int SW_RESTORE = 9;
}
}

10
AssemblyInfo.cs Normal file
View File

@ -0,0 +1,10 @@
using System.Windows;
[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)
)]

45
Converters.cs Normal file
View File

@ -0,0 +1,45 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace ShortcutsHelper
{
public class BoolToPinConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isPinned)
{
return isPinned ? "📌" : "📍";
}
return "📍";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
}

158
MainWindow.xaml Normal file
View File

@ -0,0 +1,158 @@
<Window x:Class="ShortcutsHelper.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ShortcutsHelper"
xmlns:vm="clr-namespace:ShortcutsHelper.ViewModels"
mc:Ignorable="d"
Title="Shortcuts Helper" Height="400" Width="600" Topmost="True"
ResizeMode="CanResize" WindowStyle="ToolWindow">
<Window.Resources>
<!-- Converter para el ícono de Pin -->
<local:BoolToPinConverter x:Key="BoolToPinConverter"/>
<!-- Converter para visibilidad basada en boolean -->
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header con aplicación actual y botón de Pin -->
<Border Grid.Row="0" Background="DarkBlue" Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Botón de Pin -->
<Button Grid.Column="0"
x:Name="PinButton"
Background="Transparent"
BorderThickness="0"
Foreground="White"
FontSize="16"
Margin="0,0,10,0"
ToolTip="Pin/Unpin aplicación actual"
Click="PinButton_Click">
<Button.Content>
<TextBlock Text="{Binding IsPinnedMode, Converter={StaticResource BoolToPinConverter}}" />
</Button.Content>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Cursor" Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#4000BFFF"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<!-- Información de la aplicación -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="App:" FontWeight="Bold" Foreground="White" Margin="0,0,5,0"/>
<TextBlock Text="{Binding CurrentApplication}" FontWeight="Bold" Foreground="Yellow"/>
<TextBlock Text=" - " FontWeight="Bold" Foreground="White" Margin="5,0"/>
<TextBlock Text="{Binding Shortcuts.Count, StringFormat=\{0\} shortcuts}" Foreground="LightGray"/>
<TextBlock Text=" (Pinned)" FontWeight="Bold" Foreground="Orange" Margin="5,0,0,0"
Visibility="{Binding IsPinnedMode, Converter={StaticResource BoolToVisibilityConverter}}"/>
</StackPanel>
</Grid>
</Border>
<!-- DataGrid -->
<DataGrid x:Name="ShortcutsDataGrid" Grid.Row="1"
ItemsSource="{Binding Shortcuts}"
SelectedItem="{Binding SelectedShortcut}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
AlternatingRowBackground="LightGray"
RowHeaderWidth="0"
CanUserSortColumns="False"
IsReadOnly="False"
MouseDoubleClick="ShortcutsDataGrid_MouseDoubleClick"
CellEditEnding="ShortcutsDataGrid_CellEditEnding"
RowEditEnding="ShortcutsDataGrid_RowEditEnding">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="🗑️ Eliminar" Click="MenuItem_Delete_Click" />
<Separator />
<MenuItem Header="⌨️ Capturar Teclas" Click="MenuItem_CaptureKeys_Click" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridCheckBoxColumn x:Name="FavoriteColumn"
Header="⭐"
Binding="{Binding IsFavorite, UpdateSourceTrigger=PropertyChanged}"
Width="40"
MinWidth="40"/>
<DataGridTextColumn x:Name="ShortcutColumn"
Header="Atajo"
Binding="{Binding Shortcut, UpdateSourceTrigger=PropertyChanged}"
Width="140"
MinWidth="100"
FontFamily="Consolas"
FontWeight="Bold"/>
<DataGridTemplateColumn x:Name="DescriptionColumn"
Header="Descripción"
Width="*"
MinWidth="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"
TextWrapping="Wrap"
MaxWidth="400"
Margin="5,2"
VerticalAlignment="Top"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
MinHeight="40"
MaxHeight="120"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
BorderThickness="1"
BorderBrush="LightBlue"
Margin="1"
Padding="2"
VerticalAlignment="Stretch"
VerticalContentAlignment="Top"
HorizontalAlignment="Stretch"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="MinHeight" Value="40"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsFavorite}" Value="True">
<Setter Property="Background" Value="LightYellow"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsModified}" Value="True">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</Window>

268
MainWindow.xaml.cs Normal file
View File

@ -0,0 +1,268 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using ShortcutsHelper.Models;
using ShortcutsHelper.ViewModels;
namespace ShortcutsHelper
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel ViewModel => (MainViewModel)DataContext;
private bool _isInitialized = false;
public MainWindow()
{
InitializeComponent();
this.Closing += MainWindow_Closing;
this.Loaded += MainWindow_Loaded;
this.SizeChanged += MainWindow_SizeChanged;
this.LocationChanged += MainWindow_LocationChanged;
this.Deactivated += MainWindow_Deactivated;
this.Activated += MainWindow_Activated;
// Suscribirse al evento de cambio de visibilidad
ViewModel.VisibilityChanged += OnVisibilityChanged;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
LoadWindowSettings();
this.Opacity = 1.0; // Inicializar con opacidad completa
_isInitialized = true;
}
private void OnVisibilityChanged(object? sender, bool shouldBeVisible)
{
// Ejecutar en el hilo de UI
Dispatcher.BeginInvoke(() =>
{
if (shouldBeVisible)
{
// Mostrar ventana con opacidad completa
if (this.Visibility == Visibility.Hidden)
{
this.Show();
}
if (this.WindowState == WindowState.Minimized)
{
this.WindowState = WindowState.Normal;
}
this.Opacity = 1.0; // 100% opacidad
}
else
{
// En lugar de ocultar, usar transparencia del 20%
this.Opacity = 0.2; // 20% opacidad
// Mantener la ventana visible pero semi-transparente
if (this.Visibility == Visibility.Hidden)
{
this.Show();
}
}
});
}
private void PinButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.TogglePinMode();
}
private void LoadWindowSettings()
{
try
{
var settings = ViewModel.GetCurrentApplicationSettings();
this.Left = settings.Left;
this.Top = settings.Top;
this.Width = settings.Width;
this.Height = settings.Height;
// Aplicar anchos de columna
if (ShortcutsDataGrid != null)
{
FavoriteColumn.Width = settings.FavoriteColumnWidth;
ShortcutColumn.Width = settings.ShortcutColumnWidth;
// Para DataGridTemplateColumn necesitamos manejar el ancho diferente
DescriptionColumn.Width = new DataGridLength(settings.DescriptionColumnWidth, DataGridLengthUnitType.Pixel);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al cargar configuración de ventana: {ex.Message}");
}
}
private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_isInitialized)
{
SaveWindowSettings();
}
}
private void MainWindow_LocationChanged(object? sender, EventArgs e)
{
if (_isInitialized)
{
SaveWindowSettings();
}
}
private void SaveWindowSettings()
{
try
{
ViewModel.UpdateWindowSettings(this.Left, this.Top, this.Width, this.Height);
if (ShortcutsDataGrid != null)
{
// Para DataGridTemplateColumn obtenemos el ancho diferente
double descriptionWidth = DescriptionColumn.ActualWidth;
ViewModel.UpdateColumnWidths(
FavoriteColumn.Width.Value,
ShortcutColumn.Width.Value,
descriptionWidth);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al guardar configuración de ventana: {ex.Message}");
}
}
private void MainWindow_Deactivated(object? sender, EventArgs e)
{
// Confirmar cualquier edición pendiente en el DataGrid
CommitDataGridEdits();
// Guardar cambios cuando pierde el foco
ViewModel.SavePendingChanges();
}
private void MainWindow_Activated(object? sender, EventArgs e)
{
// Cuando la ventana se activa, asegurar opacidad completa
this.Opacity = 1.0;
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
try
{
// Confirmar cualquier edición pendiente en el DataGrid
CommitDataGridEdits();
SaveWindowSettings();
// Desuscribirse del evento
ViewModel.VisibilityChanged -= OnVisibilityChanged;
ViewModel.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"Error al cerrar aplicación: {ex.Message}");
}
}
private void MenuItem_Delete_Click(object sender, RoutedEventArgs e)
{
ViewModel.DeleteSelectedShortcut();
}
private void MenuItem_CaptureKeys_Click(object sender, RoutedEventArgs e)
{
ViewModel.StartKeyCapture();
}
private void ShortcutsDataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
// Verificar si se hizo doble clic en una celda (no en el header)
if (e.OriginalSource is FrameworkElement element)
{
// Buscar si el clic fue en una celda del DataGrid
var cell = element.GetVisualParent<DataGridCell>();
if (cell != null && cell.Column == DescriptionColumn)
{
// Iniciar edición de la celda de descripción
ShortcutsDataGrid.BeginEdit();
}
}
}
private void ShortcutsDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
// Este evento se dispara cuando termina la edición de una celda
// No cancelamos la edición aquí, solo nos aseguramos de que se procese
if (!e.Cancel)
{
// Forzar la actualización del binding
Dispatcher.BeginInvoke(() =>
{
if (e.EditingElement is System.Windows.Controls.TextBox textBox)
{
var bindingExpression = textBox.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty);
bindingExpression?.UpdateSource();
}
});
}
}
private void ShortcutsDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
// Este evento se dispara cuando termina la edición de una fila
// Aquí podrían guardarse los cambios si fuera necesario
if (!e.Cancel && e.Row.Item is ShortcutRecord shortcut)
{
// Forzar guardar cambios después de que termine la edición de la fila
Dispatcher.BeginInvoke(() =>
{
if (shortcut.IsModified)
{
ViewModel.SavePendingChanges();
}
});
}
}
private void CommitDataGridEdits()
{
try
{
if (ShortcutsDataGrid != null)
{
// Terminar cualquier edición activa
ShortcutsDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
ShortcutsDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
// Actualizar el binding
ShortcutsDataGrid.UpdateLayout();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al confirmar ediciones del DataGrid: {ex.Message}");
}
}
}
// Método de extensión para buscar elementos padre en el árbol visual
public static class VisualTreeHelperExtensions
{
public static T? GetVisualParent<T>(this DependencyObject child) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(child);
if (parent == null) return null;
if (parent is T) return (T)parent;
return parent.GetVisualParent<T>();
}
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace ShortcutsHelper.Models
{
public class AppConfiguration
{
public Dictionary<string, ApplicationSettings> Applications { get; set; } = new();
public string LastActiveApplication { get; set; } = "";
public int InactivityTimeoutMinutes { get; set; } = 5;
public bool IsPinnedMode { get; set; } = true;
public string PinnedApplication { get; set; } = "";
}
}

104
Models/AppSettings.cs Normal file
View File

@ -0,0 +1,104 @@
using System.Text.Json;
using System.IO;
namespace ShortcutsHelper.Models
{
public class AppSettings
{
public Dictionary<string, ApplicationWindowSettings> ApplicationSettings { get; set; } = new();
}
public class ApplicationWindowSettings
{
public double Left { get; set; } = 100;
public double Top { get; set; } = 100;
public double Width { get; set; } = 400;
public double Height { get; set; } = 300;
public double FavoriteColumnWidth { get; set; } = 40;
public double ShortcutColumnWidth { get; set; } = 140;
public double DescriptionColumnWidth { get; set; } = 200;
}
public static class SettingsManager
{
private static readonly string SettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ShortcutsHelper",
"app-settings.json");
private static AppSettings? _settings;
public static AppSettings LoadSettings()
{
if (_settings != null)
return _settings;
try
{
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
_settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
else
{
_settings = new AppSettings();
}
}
catch
{
_settings = new AppSettings();
}
return _settings;
}
public static void SaveSettings(AppSettings settings)
{
try
{
var dir = Path.GetDirectoryName(SettingsPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsPath, json);
_settings = settings;
}
catch
{
// Ignorar errores al guardar
}
}
public static ApplicationWindowSettings GetApplicationSettings(string applicationName)
{
var settings = LoadSettings();
if (!settings.ApplicationSettings.ContainsKey(applicationName))
{
// Configuración por defecto para aplicaciones nuevas
settings.ApplicationSettings[applicationName] = new ApplicationWindowSettings
{
Left = 100,
Top = 100,
Width = 400,
Height = 300,
FavoriteColumnWidth = 40,
ShortcutColumnWidth = 140,
DescriptionColumnWidth = 200
};
SaveSettings(settings);
}
return settings.ApplicationSettings[applicationName];
}
public static void SaveApplicationSettings(string applicationName, ApplicationWindowSettings appSettings)
{
var settings = LoadSettings();
settings.ApplicationSettings[applicationName] = appSettings;
SaveSettings(settings);
}
}
}

View File

@ -0,0 +1,13 @@
namespace ShortcutsHelper.Models
{
public class ApplicationSettings
{
public double Left { get; set; } = 100;
public double Top { get; set; } = 100;
public double Width { get; set; } = 600;
public double Height { get; set; } = 400;
public double FavoriteColumnWidth { get; set; } = 40;
public double ShortcutColumnWidth { get; set; } = 150;
public double DescriptionColumnWidth { get; set; } = 350;
}
}

89
Models/ShortcutRecord.cs Normal file
View File

@ -0,0 +1,89 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ShortcutsHelper.Models
{
public class ShortcutRecord : INotifyPropertyChanged
{
private string _application = "";
private string _shortcut = "";
private string _description = "";
private bool _isFavorite = false;
private bool _isModified = false;
public string Application
{
get => _application;
set
{
if (_application != value)
{
_application = value;
OnPropertyChanged();
}
}
}
public string Shortcut
{
get => _shortcut;
set
{
if (_shortcut != value)
{
_shortcut = value;
IsModified = true;
OnPropertyChanged();
}
}
}
public string Description
{
get => _description;
set
{
if (_description != value)
{
_description = value;
IsModified = true;
OnPropertyChanged();
}
}
}
public bool IsFavorite
{
get => _isFavorite;
set
{
if (_isFavorite != value)
{
_isFavorite = value;
IsModified = true;
OnPropertyChanged();
}
}
}
public bool IsModified
{
get => _isModified;
set
{
if (_isModified != value)
{
_isModified = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Text.Json;
using ShortcutsHelper.Models;
namespace ShortcutsHelper.Services
{
public class ConfigurationService
{
private const string ConfigFileName = "shortcuts_config.json";
private static string ConfigFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ShortcutsHelper", ConfigFileName);
public AppConfiguration LoadConfiguration()
{
try
{
if (File.Exists(ConfigFilePath))
{
string json = File.ReadAllText(ConfigFilePath);
return JsonSerializer.Deserialize<AppConfiguration>(json) ?? new AppConfiguration();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al cargar configuración: {ex.Message}");
}
return new AppConfiguration();
}
public void SaveConfiguration(AppConfiguration config)
{
try
{
string directory = Path.GetDirectoryName(ConfigFilePath)!;
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var options = new JsonSerializerOptions
{
WriteIndented = true
};
string json = JsonSerializer.Serialize(config, options);
File.WriteAllText(ConfigFilePath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error al guardar configuración: {ex.Message}");
}
}
public ApplicationSettings GetApplicationSettings(AppConfiguration config, string applicationName)
{
if (config.Applications.TryGetValue(applicationName, out var settings))
{
return settings;
}
// Crear configuración por defecto para nueva aplicación
var defaultSettings = new ApplicationSettings();
config.Applications[applicationName] = defaultSettings;
return defaultSettings;
}
public void UpdateApplicationSettings(AppConfiguration config, string applicationName, ApplicationSettings settings)
{
config.Applications[applicationName] = settings;
}
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Gma.System.MouseKeyHook;
namespace ShortcutsHelper.Services
{
public class KeyCaptureService : IDisposable
{
private IKeyboardMouseEvents? _globalHook;
private bool _isCapturing = false;
private List<string> _capturedKeys = new();
public event EventHandler<string>? KeyCaptured;
public event EventHandler? CaptureCancelled;
public bool IsCapturing => _isCapturing;
public void StartCapturing()
{
if (_isCapturing) return;
_isCapturing = true;
_capturedKeys.Clear();
_globalHook = Hook.GlobalEvents();
_globalHook.KeyDown += OnKeyDown;
_globalHook.KeyUp += OnKeyUp;
}
public void StopCapturing()
{
_isCapturing = false;
if (_globalHook != null)
{
_globalHook.KeyDown -= OnKeyDown;
_globalHook.KeyUp -= OnKeyUp;
_globalHook.Dispose();
_globalHook = null;
}
}
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (!_isCapturing) return;
if (e.KeyCode == Keys.Escape)
{
StopCapturing();
CaptureCancelled?.Invoke(this, EventArgs.Empty);
return;
}
var keyString = BuildKeyString(e);
if (!string.IsNullOrEmpty(keyString))
{
_capturedKeys.Clear();
_capturedKeys.Add(keyString);
}
}
private void OnKeyUp(object? sender, KeyEventArgs e)
{
if (!_isCapturing || _capturedKeys.Count == 0) return;
StopCapturing();
KeyCaptured?.Invoke(this, _capturedKeys.First().Replace(" ", ""));
}
private string BuildKeyString(KeyEventArgs e)
{
var parts = new List<string>();
if ((Control.ModifierKeys & Keys.Control) != 0) parts.Add("Ctrl");
if ((Control.ModifierKeys & Keys.Alt) != 0) parts.Add("Alt");
if ((Control.ModifierKeys & Keys.Shift) != 0) parts.Add("Shift");
if ((Control.ModifierKeys & Keys.LWin) != 0 || (Control.ModifierKeys & Keys.RWin) != 0) parts.Add("Win");
if (e.KeyCode != Keys.ControlKey && e.KeyCode != Keys.LControlKey && e.KeyCode != Keys.RControlKey &&
e.KeyCode != Keys.Menu && e.KeyCode != Keys.LMenu && e.KeyCode != Keys.RMenu &&
e.KeyCode != Keys.ShiftKey && e.KeyCode != Keys.LShiftKey && e.KeyCode != Keys.RShiftKey &&
e.KeyCode != Keys.LWin && e.KeyCode != Keys.RWin)
{
parts.Add(GetKeyName(e.KeyCode));
}
return string.Join("+", parts);
}
private string GetKeyName(Keys key)
{
return key switch
{
Keys.Space => "Space",
Keys.Enter => "Enter",
Keys.Tab => "Tab",
Keys.Back => "Backspace",
Keys.Delete => "Delete",
Keys.Insert => "Insert",
Keys.Home => "Home",
Keys.End => "End",
Keys.PageUp => "PageUp",
Keys.PageDown => "PageDown",
Keys.Up => "↑",
Keys.Down => "↓",
Keys.Left => "←",
Keys.Right => "→",
Keys.F1 => "F1",
Keys.F2 => "F2",
Keys.F3 => "F3",
Keys.F4 => "F4",
Keys.F5 => "F5",
Keys.F6 => "F6",
Keys.F7 => "F7",
Keys.F8 => "F8",
Keys.F9 => "F9",
Keys.F10 => "F10",
Keys.F11 => "F11",
Keys.F12 => "F12",
_ => key.ToString()
};
}
public void Dispose()
{
StopCapturing();
}
}
}

232
Services/ShortcutService.cs Normal file
View File

@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Linq;
using libObsidean;
using ShortcutsHelper.Models;
using System.IO;
using Newtonsoft.Json.Linq;
namespace ShortcutsHelper.Services
{
public class ShortcutService
{
private readonly Obsidean _obsidean = new();
private readonly Dictionary<string, List<ShortcutRecord>> _shortcutsByApplication = new();
private bool _isLoaded = false;
public event EventHandler<string>? ApplicationShortcutsChanged;
public void LoadAllShortcuts()
{
if (_isLoaded) return;
try
{
var tabla = _obsidean.LeerShortcuts();
if (tabla != null && tabla.GetLength(0) > 1)
{
_shortcutsByApplication.Clear();
for (int i = 1; i < tabla.GetLength(0); i++)
{
string app = tabla[i, 0] ?? "";
string shortcut = tabla[i, 1] ?? "";
string description = tabla[i, 2] ?? "";
if (!string.IsNullOrWhiteSpace(app) && (!string.IsNullOrWhiteSpace(shortcut) || !string.IsNullOrWhiteSpace(description)))
{
bool isFavorite = false;
if (tabla.GetLength(1) > 3 && !string.IsNullOrWhiteSpace(tabla[i, 3]))
{
bool.TryParse(tabla[i, 3], out isFavorite);
}
var record = new ShortcutRecord
{
Application = app,
Shortcut = shortcut,
Description = description,
IsFavorite = isFavorite,
IsModified = false
};
if (!_shortcutsByApplication.ContainsKey(app))
{
_shortcutsByApplication[app] = new List<ShortcutRecord>();
}
_shortcutsByApplication[app].Add(record);
}
}
}
_isLoaded = true;
}
catch (Exception ex)
{
Console.WriteLine($"Error al cargar shortcuts: {ex.Message}");
}
}
public List<ShortcutRecord> GetShortcutsForApplication(string applicationName)
{
if (!_isLoaded) LoadAllShortcuts();
if (_shortcutsByApplication.TryGetValue(applicationName, out var shortcuts))
{
return shortcuts.OrderByDescending(s => s.IsFavorite).ThenBy(s => s.Shortcut).ToList();
}
return new List<ShortcutRecord>();
}
public void AddOrUpdateShortcut(ShortcutRecord shortcut)
{
if (string.IsNullOrWhiteSpace(shortcut.Application)) return;
if (!_shortcutsByApplication.ContainsKey(shortcut.Application))
{
_shortcutsByApplication[shortcut.Application] = new List<ShortcutRecord>();
}
var existing = _shortcutsByApplication[shortcut.Application]
.FirstOrDefault(s => s.Shortcut == shortcut.Shortcut);
if (existing != null)
{
existing.Description = shortcut.Description;
existing.IsFavorite = shortcut.IsFavorite;
// No marcar como IsModified = false aquí, dejar que SaveAllShortcuts() lo haga
}
else
{
// No marcar como IsModified = false aquí, dejar que SaveAllShortcuts() lo haga
_shortcutsByApplication[shortcut.Application].Add(shortcut);
}
ApplicationShortcutsChanged?.Invoke(this, shortcut.Application);
}
public void RemoveShortcut(string applicationName, string shortcut)
{
if (string.IsNullOrWhiteSpace(applicationName) || string.IsNullOrWhiteSpace(shortcut)) return;
if (_shortcutsByApplication.TryGetValue(applicationName, out var shortcuts))
{
var toRemove = shortcuts.FirstOrDefault(s => s.Shortcut == shortcut);
if (toRemove != null)
{
shortcuts.Remove(toRemove);
ApplicationShortcutsChanged?.Invoke(this, applicationName);
}
}
}
public void SaveAllShortcuts()
{
try
{
var allShortcuts = new List<ShortcutRecord>();
foreach (var appShortcuts in _shortcutsByApplication.Values)
{
allShortcuts.AddRange(appShortcuts.Where(s => !string.IsNullOrWhiteSpace(s.Shortcut)));
}
if (allShortcuts.Count == 0) return;
// Crear tabla para guardar
var tabla = new string[allShortcuts.Count + 1, 4];
tabla[0, 0] = "Application";
tabla[0, 1] = "Shortcut";
tabla[0, 2] = "Description";
tabla[0, 3] = "IsFavorite";
for (int i = 0; i < allShortcuts.Count; i++)
{
var shortcut = allShortcuts[i];
tabla[i + 1, 0] = shortcut.Application;
tabla[i + 1, 1] = shortcut.Shortcut;
tabla[i + 1, 2] = shortcut.Description;
tabla[i + 1, 3] = shortcut.IsFavorite.ToString();
}
// Guardar en Obsidian
var vaultPath = GetVaultPath("VM");
if (!string.IsNullOrEmpty(vaultPath))
{
string pathToMarkdown = Path.Combine(vaultPath, "DB", "Shortcuts", "Shortcuts.md");
libObsidean.Obsidean.SaveTableToMarkdown(pathToMarkdown, tabla);
// Marcar todos como no modificados
foreach (var appShortcuts in _shortcutsByApplication.Values)
{
foreach (var shortcut in appShortcuts)
{
shortcut.IsModified = false;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al guardar shortcuts: {ex.Message}");
}
}
public bool HasModifiedShortcuts()
{
return _shortcutsByApplication.Values
.Any(shortcuts => shortcuts.Any(s => s.IsModified));
}
public List<string> GetAllApplicationNames()
{
if (!_isLoaded) LoadAllShortcuts();
return _shortcutsByApplication.Keys.OrderBy(k => k).ToList();
}
private string? GetVaultPath(string vaultName)
{
try
{
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string pathToJsonFile = Path.Combine(appDataPath, "obsidian", "obsidian.json");
if (File.Exists(pathToJsonFile))
{
string jsonContent = File.ReadAllText(pathToJsonFile);
var jsonObject = JObject.Parse(jsonContent);
var vaults = jsonObject["vaults"] as JObject;
if (vaults != null)
{
foreach (var vault in vaults)
{
var pathToken = vault.Value?["path"];
if (pathToken != null)
{
string? path = pathToken.ToString();
if (!string.IsNullOrEmpty(path))
{
string? dirPath = Path.GetDirectoryName(path.TrimEnd('\\') + "\\");
if (!string.IsNullOrEmpty(dirPath))
{
string lastDirectoryName = Path.GetFileName(dirPath);
if (lastDirectoryName.Equals(vaultName, StringComparison.OrdinalIgnoreCase))
{
return path;
}
}
}
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Error al leer vault: " + ex.Message);
}
return null;
}
}
}

View File

@ -0,0 +1,13 @@
{
"folders": [
{
"path": "."
},
{
"path": "../Libraries/libObsidean"
}
],
"settings": {
"dotnet.defaultSolution": "ShortcutsHelper/ShortcutsHelper.sln"
}
}

36
ShortcutsHelper.csproj Normal file
View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="MouseKeyHook" Version="5.7.1" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Libraries\libObsidean\libObsidean.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="C:\Trabajo\Graphics\Icons\close.png" Link="Icons\close.png" />
<Resource Include="C:\Trabajo\Graphics\Icons\delete.png" Link="Icons\delete.png" />
<Resource Include="C:\Trabajo\Graphics\Icons\add-button.png" Link="Icons\add-button.png" />
<Resource Include="C:\Trabajo\Graphics\Icons\remove.png" Link="Icons\remove.png" />
<Resource Include="C:\Trabajo\Graphics\Icons\use.png" Link="Icons\use.png" />
</ItemGroup>
<ItemGroup>
<Folder Include="Icons\" />
</ItemGroup>
</Project>

25
ShortcutsHelper.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShortcutsHelper", "ShortcutsHelper.csproj", "{B3511550-5621-491D-A554-412664C38E4B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B3511550-5621-491D-A554-412664C38E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3511550-5621-491D-A554-412664C38E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3511550-5621-491D-A554-412664C38E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3511550-5621-491D-A554-412664C38E4B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {17860D8C-E283-4288-89AA-CC11A43770EC}
EndGlobalSection
EndGlobal

412
ViewModels/MainViewModel.cs Normal file
View File

@ -0,0 +1,412 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ShortcutsHelper.Models;
using ShortcutsHelper.Services;
using WpfApplication = System.Windows.Application;
using SystemTimer = System.Timers.Timer;
namespace ShortcutsHelper.ViewModels
{
public class MainViewModel : INotifyPropertyChanged, IDisposable
{
private readonly ShortcutService _shortcutService;
private readonly ConfigurationService _configService;
private readonly KeyCaptureService _keyCaptureService;
private readonly SystemTimer _appDetectionTimer;
private readonly SystemTimer _inactivityTimer;
private string _currentApplication = "";
private ShortcutRecord? _selectedShortcut;
private string _originalDescription = "";
private bool _isInitialized = false;
private bool _isPinnedMode = true; // Por defecto está activo
public ObservableCollection<ShortcutRecord> Shortcuts { get; } = new();
public string CurrentApplication
{
get => _currentApplication;
private set
{
if (_currentApplication != value)
{
_currentApplication = value;
OnPropertyChanged();
LoadShortcutsForCurrentApp();
}
}
}
public ShortcutRecord? SelectedShortcut
{
get => _selectedShortcut;
set
{
if (_selectedShortcut != value)
{
_selectedShortcut = value;
OnPropertyChanged();
EnsureEmptyRows();
}
}
}
public bool IsCapturingKeys => _keyCaptureService.IsCapturing;
public bool IsPinnedMode
{
get => _isPinnedMode;
set
{
if (_isPinnedMode != value)
{
_isPinnedMode = value;
OnPropertyChanged();
// Actualizar configuración
_appConfig.IsPinnedMode = value;
if (value)
{
// Al activar Pin, guardar la aplicación actual como pinned (si no está vacía)
if (!string.IsNullOrEmpty(CurrentApplication))
{
_appConfig.PinnedApplication = CurrentApplication;
}
}
else
{
// Al desactivar Pin, limpiar la aplicación pinned y mostrar la ventana
_appConfig.PinnedApplication = "";
VisibilityChanged?.Invoke(this, true); // Mostrar ventana cuando se desactiva Pin
}
_configService.SaveConfiguration(_appConfig);
}
}
}
// Configuración de aplicación actual
private AppConfiguration _appConfig = new();
private ApplicationSettings? _currentAppSettings;
// Evento para notificar cambios de visibilidad de la ventana
public event EventHandler<bool>? VisibilityChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public MainViewModel()
{
_shortcutService = new ShortcutService();
_configService = new ConfigurationService();
_keyCaptureService = new KeyCaptureService();
// Configurar timers
_appDetectionTimer = new SystemTimer(1000); // Cada segundo
_appDetectionTimer.Elapsed += OnAppDetectionTimer;
_inactivityTimer = new SystemTimer(TimeSpan.FromMinutes(5).TotalMilliseconds); // 5 minutos por defecto
_inactivityTimer.Elapsed += OnInactivityTimer;
// Suscribirse a eventos
_keyCaptureService.KeyCaptured += OnKeyCaptured;
_keyCaptureService.CaptureCancelled += OnCaptureCancelled;
_shortcutService.ApplicationShortcutsChanged += OnApplicationShortcutsChanged;
Initialize();
}
public void Initialize()
{
if (_isInitialized) return;
// Cargar configuración
_appConfig = _configService.LoadConfiguration();
_inactivityTimer.Interval = TimeSpan.FromMinutes(_appConfig.InactivityTimeoutMinutes).TotalMilliseconds;
// Restaurar estado de Pin
_isPinnedMode = _appConfig.IsPinnedMode;
OnPropertyChanged(nameof(IsPinnedMode));
// Cargar todos los shortcuts de una vez
_shortcutService.LoadAllShortcuts();
// Si está en modo Pin y no hay aplicación pinned, establecer la primera aplicación detectada
// Pero primero detectar la aplicación actual para establecer CurrentApplication
string initialApp = GetActiveProcessName();
if (!string.IsNullOrWhiteSpace(initialApp) &&
!initialApp.Equals("ShortcutsHelper", StringComparison.OrdinalIgnoreCase))
{
CurrentApplication = initialApp;
LoadApplicationSettings();
// Si está en modo Pin y no hay aplicación pinned, establecer esta como pinned
if (IsPinnedMode && string.IsNullOrEmpty(_appConfig.PinnedApplication))
{
_appConfig.PinnedApplication = initialApp;
_configService.SaveConfiguration(_appConfig);
}
}
// Iniciar detección de aplicación
_appDetectionTimer.Start();
// Iniciar timer de inactividad
_inactivityTimer.Start();
_isInitialized = true;
}
private void OnAppDetectionTimer(object? sender, System.Timers.ElapsedEventArgs e)
{
DetectActiveApplication();
}
private void OnInactivityTimer(object? sender, System.Timers.ElapsedEventArgs e)
{
// Guardar cambios por inactividad
SavePendingChanges();
}
private void DetectActiveApplication()
{
string appName = GetActiveProcessName();
if (string.IsNullOrWhiteSpace(appName))
return;
// Si está en modo Pin
if (IsPinnedMode)
{
// Si no hay aplicación pinned, establecer la aplicación actual (excepto ShortcutsHelper)
if (string.IsNullOrEmpty(_appConfig.PinnedApplication) &&
!appName.Equals("ShortcutsHelper", StringComparison.OrdinalIgnoreCase))
{
_appConfig.PinnedApplication = appName;
CurrentApplication = appName;
LoadApplicationSettings();
_configService.SaveConfiguration(_appConfig);
return;
}
// Si hay aplicación pinned, manejar visibilidad
if (!string.IsNullOrEmpty(_appConfig.PinnedApplication))
{
bool isShortcutsHelper = appName.Equals("ShortcutsHelper", StringComparison.OrdinalIgnoreCase);
bool isPinnedApp = appName.Equals(_appConfig.PinnedApplication, StringComparison.OrdinalIgnoreCase);
// Mostrar ventana solo si está en ShortcutsHelper o en la aplicación pinned
bool shouldBeVisible = isShortcutsHelper || isPinnedApp;
VisibilityChanged?.Invoke(this, shouldBeVisible);
// No cambiar la aplicación actual cuando está pinned
return;
}
}
// Modo normal: cambiar aplicación pero no mostrar ShortcutsHelper
if (appName.Equals("ShortcutsHelper", StringComparison.OrdinalIgnoreCase))
return;
if (appName != CurrentApplication)
{
SaveCurrentApplicationSettings();
CurrentApplication = appName;
LoadApplicationSettings();
}
}
public void TogglePinMode()
{
IsPinnedMode = !IsPinnedMode;
}
private void LoadShortcutsForCurrentApp()
{
WpfApplication.Current.Dispatcher.Invoke(() =>
{
Shortcuts.Clear();
var shortcuts = _shortcutService.GetShortcutsForApplication(CurrentApplication);
foreach (var shortcut in shortcuts)
{
Shortcuts.Add(shortcut);
}
EnsureEmptyRows();
});
}
private void EnsureEmptyRows()
{
WpfApplication.Current.Dispatcher.BeginInvoke(() =>
{
var emptyRows = Shortcuts.Where(s => string.IsNullOrWhiteSpace(s.Shortcut) && string.IsNullOrWhiteSpace(s.Description)).Count();
while (emptyRows < 3)
{
Shortcuts.Add(new ShortcutRecord { Application = CurrentApplication });
emptyRows++;
}
});
}
private void OnApplicationShortcutsChanged(object? sender, string applicationName)
{
if (applicationName == CurrentApplication)
{
LoadShortcutsForCurrentApp();
}
}
public void DeleteSelectedShortcut()
{
if (SelectedShortcut != null && !string.IsNullOrWhiteSpace(SelectedShortcut.Shortcut))
{
_shortcutService.RemoveShortcut(CurrentApplication, SelectedShortcut.Shortcut);
Shortcuts.Remove(SelectedShortcut);
EnsureEmptyRows();
}
}
public void StartKeyCapture()
{
if (SelectedShortcut == null) return;
if (_keyCaptureService.IsCapturing)
{
_keyCaptureService.StopCapturing();
return;
}
_originalDescription = SelectedShortcut.Description;
SelectedShortcut.Description = "Presiona las teclas... (Esc para cancelar)";
_keyCaptureService.StartCapturing();
OnPropertyChanged(nameof(IsCapturingKeys));
}
private void OnKeyCaptured(object? sender, string keyString)
{
if (SelectedShortcut != null)
{
SelectedShortcut.Shortcut = keyString;
SelectedShortcut.Description = _originalDescription;
SelectedShortcut.IsModified = true;
}
OnPropertyChanged(nameof(IsCapturingKeys));
}
private void OnCaptureCancelled(object? sender, EventArgs e)
{
if (SelectedShortcut != null)
{
SelectedShortcut.Description = _originalDescription;
}
OnPropertyChanged(nameof(IsCapturingKeys));
}
public void SavePendingChanges()
{
try
{
// Guardar shortcuts modificados
var modifiedShortcuts = Shortcuts.Where(s => s.IsModified && !string.IsNullOrWhiteSpace(s.Shortcut)).ToList();
foreach (var shortcut in modifiedShortcuts)
{
_shortcutService.AddOrUpdateShortcut(shortcut);
}
if (_shortcutService.HasModifiedShortcuts())
{
_shortcutService.SaveAllShortcuts();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error al guardar cambios: {ex.Message}");
}
}
public ApplicationSettings GetCurrentApplicationSettings()
{
return _configService.GetApplicationSettings(_appConfig, CurrentApplication);
}
public void SaveCurrentApplicationSettings()
{
if (string.IsNullOrEmpty(CurrentApplication) || _currentAppSettings == null) return;
_configService.UpdateApplicationSettings(_appConfig, CurrentApplication, _currentAppSettings);
_configService.SaveConfiguration(_appConfig);
}
private void LoadApplicationSettings()
{
_currentAppSettings = _configService.GetApplicationSettings(_appConfig, CurrentApplication);
}
public void UpdateWindowSettings(double left, double top, double width, double height)
{
if (_currentAppSettings != null)
{
_currentAppSettings.Left = left;
_currentAppSettings.Top = top;
_currentAppSettings.Width = width;
_currentAppSettings.Height = height;
}
}
public void UpdateColumnWidths(double favoriteWidth, double shortcutWidth, double descriptionWidth)
{
if (_currentAppSettings != null)
{
_currentAppSettings.FavoriteColumnWidth = favoriteWidth;
_currentAppSettings.ShortcutColumnWidth = shortcutWidth;
_currentAppSettings.DescriptionColumnWidth = descriptionWidth;
}
}
private static string GetActiveProcessName()
{
IntPtr hWnd = GetForegroundWindow();
if (hWnd == IntPtr.Zero) return string.Empty;
GetWindowThreadProcessId(hWnd, out uint pid);
try
{
var proc = Process.GetProcessById((int)pid);
return proc.ProcessName;
}
catch
{
return string.Empty;
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Dispose()
{
SavePendingChanges();
SaveCurrentApplicationSettings();
_appDetectionTimer?.Stop();
_appDetectionTimer?.Dispose();
_inactivityTimer?.Stop();
_inactivityTimer?.Dispose();
_keyCaptureService?.Dispose();
}
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
}
}