diff --git a/assets/annotationSidePanel.html b/assets/annotationSidePanel.html new file mode 100644 index 0000000..c879077 --- /dev/null +++ b/assets/annotationSidePanel.html @@ -0,0 +1,144 @@ + + + + + + +
+ + + diff --git a/assets/images/icons/sideBarAnnotation.png b/assets/images/icons/sideBarAnnotation.png new file mode 100644 index 0000000..a7911c8 Binary files /dev/null and b/assets/images/icons/sideBarAnnotation.png differ diff --git a/libs/Patagames.Pdf.Wpf b/libs/Patagames.Pdf.Wpf index fb9de33..cb9fec0 160000 --- a/libs/Patagames.Pdf.Wpf +++ b/libs/Patagames.Pdf.Wpf @@ -1 +1 @@ -Subproject commit fb9de33775255e12a1d37d3de033e432fc54b193 +Subproject commit cb9fec09a122cece3f24e1671d3fc62db4787bfb diff --git a/src/SuperMemoAssistant.Plugins.PDF/Models/PDFAnnotationHighlight.cs b/src/SuperMemoAssistant.Plugins.PDF/Models/PDFAnnotationHighlight.cs new file mode 100644 index 0000000..6574afa --- /dev/null +++ b/src/SuperMemoAssistant.Plugins.PDF/Models/PDFAnnotationHighlight.cs @@ -0,0 +1,88 @@ +#region License & Metadata + +// The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +// +// Created On: 2018/12/23 17:09 +// Modified On: 2019/02/22 13:43 +// Modified By: Alexis + +#endregion + + + + +using Newtonsoft.Json; +using Patagames.Pdf.Net.Controls.Wpf; +using System; +using System.Collections.Generic; + +namespace SuperMemoAssistant.Plugins.PDF.Models +{ + public class AnnotationAddedEventArgs : EventArgs + { + public PDFAnnotationHighlight NewItem { get; set; } + } + + public class PDFAnnotationHighlight : PDFTextExtract + { + #region Properties & Fields - Public + + [JsonProperty(PropertyName = "HTML")] + public string HtmlContent { get; set; } + [JsonProperty(PropertyName = "AnnotationId")] + public int AnnotationId { get; set; } + + #endregion + + + + + #region Methods + public static implicit operator PDFAnnotationHighlight(SelectInfo selInfo) + { + return new PDFAnnotationHighlight + { + StartPage = selInfo.StartPage, + StartIndex = selInfo.StartIndex, + EndPage = selInfo.EndPage, + EndIndex = selInfo.EndIndex, + HtmlContent = "
", + AnnotationId = 0 + }; + } + + public static PDFAnnotationHighlight Create(SelectInfo selInfo, int annotationId, string initialContent) + => new PDFAnnotationHighlight + { + StartPage = selInfo.StartPage, + StartIndex = selInfo.StartIndex, + EndPage = selInfo.EndPage, + EndIndex = selInfo.EndIndex, + HtmlContent = "
"+initialContent+"
", + AnnotationId = annotationId + }; + + public int GetSortingKey() => StartPage * 10000 + StartIndex; + + #endregion + } +} diff --git a/src/SuperMemoAssistant.Plugins.PDF/Models/PDFCfg.cs b/src/SuperMemoAssistant.Plugins.PDF/Models/PDFCfg.cs index 78aaa05..9bf8016 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/Models/PDFCfg.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/Models/PDFCfg.cs @@ -161,6 +161,14 @@ public class PDFCfg : CfgBase, INotifyPropertyChangedEx [Value(Must.MatchPattern, HexREPattern)] [JsonConverter(typeof(ColorToStringJsonConverter))] public Color IgnoreHighlightColor { get; set; } = SMConst.Stylesheet.IgnoreColor; + public Color FocusedAnnotationHighlightColor { get; set; } = Color.FromArgb(150, + 0, + 255, + 0); + public Color AnnotationHighlightColor { get; set; } = Color.FromArgb(90, + 100, + 255, + 100); public double WindowTop { get; set; } = 100; public double WindowHeight { get; set; } = 600; @@ -168,7 +176,8 @@ public class PDFCfg : CfgBase, INotifyPropertyChangedEx public double WindowWidth { get; set; } = 800; public WindowState WindowState { get; set; } = WindowState.Normal; - public double SidePanelWidth { get; set; } = 256; + public double SidePanelBookmarksWidth { get; set; } = 256; + public double SidePanelAnnotationsWidth { get; set; } = 256; // Dictionary diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFElement.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFElement.cs index c0c12cc..5cd93cf 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFElement.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFElement.cs @@ -28,6 +28,7 @@ namespace SuperMemoAssistant.Plugins.PDF.PDF { using System; + using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; @@ -60,24 +61,29 @@ public class PDFElement : INotifyPropertyChanged { #region Constructors + public delegate void AnnotationAdded(object sender, AnnotationAddedEventArgs e); + public event AnnotationAdded OnAnnotationAdded; + public PDFElement() { - BinaryMemberId = -1; - StartPage = -1; - EndPage = -1; - StartIndex = -1; - EndIndex = -1; - ReadPage = 0; - ReadPoint = default; - PDFExtracts = new ObservableCollection(); - SMExtracts = new ObservableCollection(); - SMImgExtracts = new ObservableCollection(); + BinaryMemberId = -1; + StartPage = -1; + EndPage = -1; + StartIndex = -1; + EndIndex = -1; + ReadPage = 0; + ReadPoint = default; + PDFExtracts = new ObservableCollection(); + SMExtracts = new ObservableCollection(); + SMImgExtracts = new ObservableCollection(); IgnoreHighlights = new ObservableCollection(); + AnnotationHighlights = new Dictionary>(); - PDFExtracts.CollectionChanged += OnCollectionChanged; - SMExtracts.CollectionChanged += OnCollectionChanged; - SMImgExtracts.CollectionChanged += OnCollectionChanged; + PDFExtracts.CollectionChanged += OnCollectionChanged; + SMExtracts.CollectionChanged += OnCollectionChanged; + SMImgExtracts.CollectionChanged += OnCollectionChanged; IgnoreHighlights.CollectionChanged += OnCollectionChanged; + OnAnnotationAdded += OnAnnotationAddedEffect; } #endregion @@ -107,6 +113,8 @@ public PDFElement() public ObservableCollection SMImgExtracts { get; } [JsonProperty(PropertyName = "IH")] public ObservableCollection IgnoreHighlights { get; } + [JsonProperty(PropertyName = "AH")] + public Dictionary> AnnotationHighlights { get; } [JsonProperty(PropertyName = "RPg")] public int ReadPage { get; set; } @@ -501,6 +509,7 @@ private string GetJsonB64() string elementJson = JsonConvert.SerializeObject(this, Formatting.None); + //MessageBox.Show("GetJsonB64: " + elementJson.Replace("HTML", "\nHTML")); // TODO NOCHECKIN return elementJson.ToBase64(); } @@ -581,6 +590,27 @@ private void OnCollectionChanged(object sender, IsChanged = true; } + [SuppressPropertyChangedWarnings] + private void OnAnnotationAddedEffect(object sender, + AnnotationAddedEventArgs e) + { + IsChanged = true; + } + + public void AddAnnotationHighlight(PDFAnnotationHighlight annotationHighlight) + { + OnAnnotationAdded(this, new AnnotationAddedEventArgs() { + NewItem = annotationHighlight + }); + + int page = annotationHighlight.StartPage; + if (!AnnotationHighlights.ContainsKey(page)) + { + AnnotationHighlights[page] = new List(); + } + AnnotationHighlights[page].Add(annotationHighlight); + } + #endregion diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml index b19f187..f9d6a7d 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml @@ -11,124 +11,138 @@ xmlns:dictUi="clr-namespace:SuperMemoAssistant.Plugins.Dictionary.Interop.UI;assembly=SuperMemoAssistant.Plugins.Dictionary.Interop" mc:Ignorable="d" Title="SuperMemo Incremental PDF" Height="450" Width="800" - KeyDown="Window_KeyDown"> + KeyDown="Window_KeyDown" + PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown"> + - - - - - - - - + + + + + + - + - - - - + - - - + - - + + - - - - - - - + + + + + + + + + - - - + - - - + - + + + - + + - - - - - - - - - - + + + + + + + + + - - - - - + + - + - - - - + + - - - - - - + + + + - - - - - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - - + - - + + - - - - + - - - - - + - - + + - - - - - - + + + - \ No newline at end of file + diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml.cs index 3a75bc8..401eac2 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/PDFWindow.xaml.cs @@ -31,18 +31,23 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; +using System.Windows.Interop; using System.Windows.Media; using Microsoft.Win32; using Patagames.Pdf.Net; using SuperMemoAssistant.Extensions; using SuperMemoAssistant.Plugins.PDF.Models; +using SuperMemoAssistant.Plugins.PDF.PDF.Viewer.WebBrowserWrapper; using SuperMemoAssistant.Services; using SuperMemoAssistant.Services.IO.HotKeys; using SuperMemoAssistant.Sys.IO.Devices; @@ -53,6 +58,60 @@ namespace SuperMemoAssistant.Plugins.PDF.PDF { + public class WindowHandleInfo + { + private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam); + + private IntPtr _MainHandle; + + public WindowHandleInfo(IntPtr handle) + { + this._MainHandle = handle; + } + + public List GetAllChildHandles() + { + List childHandles = new List(); + + GCHandle gcChildhandlesList = GCHandle.Alloc(childHandles); + IntPtr pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList); + + try + { + EnumWindowProc childProc = new EnumWindowProc(EnumWindow); + EnumChildWindows(this._MainHandle, childProc, pointerChildHandlesList); + } + finally + { + gcChildhandlesList.Free(); + } + + return childHandles; + } + + private bool EnumWindow(IntPtr hWnd, IntPtr lParam) + { + GCHandle gcChildhandlesList = GCHandle.FromIntPtr(lParam); + + if (gcChildhandlesList == null || gcChildhandlesList.Target == null) + { + return false; + } + + List childHandles = gcChildhandlesList.Target as List; + childHandles.Add(hWnd); + + return true; + } + } + + + + /// Interaction logic for PDFWindow.xaml partial class PDFWindow : Window { @@ -68,9 +127,11 @@ partial class PDFWindow : Window #region Properties & Fields - Non-Public protected readonly DelayedTask _saveConfigDelayed; - protected double _lastSidePanelWidth; + protected double _lastSidePanelBookmarksWidth; + protected double _lastSidePanelAnnotationsWidth; protected PDFCfg Config => PDFState.Instance.Config; + protected PDFAnnotationWebBrowserWrapper AnnotationWebBrowserWrapper { get; set; } #endregion @@ -93,8 +154,10 @@ public PDFWindow() ? WindowState.Maximized : WindowState.Normal; - if (double.IsNaN(Config.SidePanelWidth) == false) - sidePanelColumn.Width = new GridLength(Config.SidePanelWidth); + if (double.IsNaN(Config.SidePanelBookmarksWidth) == false) + sidePanelBookmarksColumn.Width = new GridLength(Config.SidePanelBookmarksWidth); + if (double.IsNaN(Config.SidePanelAnnotationsWidth) == false) + sidePanelAnnotationsColumn.Width = new GridLength(Config.SidePanelAnnotationsWidth); _saveConfigDelayed = new DelayedTask(SaveConfig); } @@ -146,7 +209,8 @@ protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) if (Equals(e.NewFocus, tvBookmarks) - || e.NewFocus is TreeViewItem) + || e.NewFocus is TreeViewItem + || e.NewFocus is WebBrowser) return; IPDFViewer.Focus(); @@ -171,9 +235,17 @@ private void IPDFViewer_OnDocumentLoaded(object sender, Bookmarks.Clear(); + AnnotationWebBrowserWrapper = new PDFAnnotationWebBrowserWrapper(wfHost, IPDFViewer); + AnnotationWebBrowserWrapper.AnnotationWebBrowser.DocumentCompleted += AnnotationWebBrowser_DocumentCompleted; + IPDFViewer.Document?.Bookmarks.ForEach(b => Bookmarks.Add(b)); } + private void AnnotationWebBrowser_DocumentCompleted(object sender, System.Windows.Forms.WebBrowserDocumentCompletedEventArgs e) + { + InstallHook(); + } + private void IPDFViewer_OnDocumentClosing(object sender, EventArgs e) { @@ -232,7 +304,8 @@ private void SaveConfig() Width, WindowState); - Config.SidePanelWidth = _lastSidePanelWidth; + Config.SidePanelBookmarksWidth = _lastSidePanelBookmarksWidth; + Config.SidePanelAnnotationsWidth = _lastSidePanelAnnotationsWidth; PDFState.Instance.SaveConfigAsync().RunAsync(); } ); @@ -243,6 +316,20 @@ public void CancelSave() IPDFViewer?.CancelSave(); } + private void Window_PreviewMouseLeftButtonDown(object sender, + EventArgs e) + { + if (IPDFViewer == null) + return; + + var currentHighlightAnnotation = IPDFViewer?.CurrentAnnotationHighlight; + + if (currentHighlightAnnotation != null) + { + AnnotationWebBrowserWrapper.ScrollToAnnotation(currentHighlightAnnotation); + } + } + private void Window_KeyDown(object sender, KeyEventArgs e) { @@ -267,6 +354,12 @@ private void Window_KeyDown(object sender, e.Handled = true; break; + case PDFHotKeys.UIToggleAnnotations: + btnAnnotations.IsChecked = !btnAnnotations.IsChecked; + AnnotationWebBrowserWrapper.AnnotationWebBrowser.Focus(); + e.Handled = true; + break; + case PDFHotKeys.UIFocusViewer: IPDFViewer.Focus(); e.Handled = true; @@ -378,18 +471,30 @@ private void TvBookmarks_PreviewKeyDown(object sender, } } - private void BtnExpandAll_Click(object sender, + private void BtnBookmarksExpandAll_Click(object sender, RoutedEventArgs e) { tvBookmarks.ExpandAll(); } - private void BtnCollapseAll_Click(object sender, + private void BtnBookmarksCollapseAll_Click(object sender, RoutedEventArgs e) { tvBookmarks.CollapseAll(); } + private void BtnAnnotationsExpandAll_Click(object sender, + RoutedEventArgs e) + { + // TODO + } + + private void BtnAnnotationsCollapseAll_Click(object sender, + RoutedEventArgs e) + { + // TODO + } + protected KeyModifiers GetKeyboardModifiers() { KeyModifiers mod = KeyModifiers.None; @@ -409,60 +514,121 @@ protected KeyModifiers GetKeyboardModifiers() private void BtnBookmarks_CheckedChanged(object sender, RoutedEventArgs e) { - if (sidePanel == null) + if (sidePanelBookmarks == null) return; bool isVisible = btnBookmarks.IsChecked ?? false; if (isVisible) { - if (sidePanel.Visibility == Visibility.Hidden) - sidePanelColumn.Width = new GridLength(Math.Max(_lastSidePanelWidth, 250)); + if (sidePanelBookmarks.Visibility == Visibility.Hidden) + sidePanelBookmarksColumn.Width = new GridLength(Math.Max(_lastSidePanelBookmarksWidth, 250)); + } + + else + { + if (sidePanelBookmarks.Visibility == Visibility.Visible) + { + _lastSidePanelBookmarksWidth = sidePanelBookmarksColumn.ActualWidth; + + sidePanelBookmarksColumn.Width = new GridLength(0); + } + } + } + + private void BtnAnnotations_CheckedChanged(object sender, + RoutedEventArgs e) + { + if (sidePanelAnnotations == null) + return; + + bool isVisible = btnAnnotations.IsChecked ?? false; + + if (isVisible) + { + if (sidePanelAnnotations.Visibility == Visibility.Hidden) + sidePanelAnnotationsColumn.Width = new GridLength(Math.Max(_lastSidePanelAnnotationsWidth, 250)); } else { - if (sidePanel.Visibility == Visibility.Visible) + if (sidePanelAnnotations.Visibility == Visibility.Visible) { - _lastSidePanelWidth = sidePanelColumn.ActualWidth; + _lastSidePanelAnnotationsWidth = sidePanelAnnotationsColumn.ActualWidth; - sidePanelColumn.Width = new GridLength(0); + sidePanelAnnotationsColumn.Width = new GridLength(0); } } } - private void SidePanel_SizeChanged(object sender, + private void SidePanelBookmarks_SizeChanged(object sender, SizeChangedEventArgs e) { - if (sidePanel.Visibility == Visibility.Visible) + if (sidePanelBookmarks.Visibility == Visibility.Visible) { - if (sidePanel.ActualWidth < 50) + if (sidePanelBookmarks.ActualWidth < 50) { - sidePanel.Visibility = Visibility.Hidden; - sidePanelColumn.Width = new GridLength(0); + sidePanelBookmarks.Visibility = Visibility.Hidden; + sidePanelBookmarksColumn.Width = new GridLength(0); btnBookmarks.IsChecked = false; - _lastSidePanelWidth = 0; + _lastSidePanelBookmarksWidth = 0; } else { - _lastSidePanelWidth = sidePanelColumn.ActualWidth; + _lastSidePanelBookmarksWidth = sidePanelBookmarksColumn.ActualWidth; } } - else if (sidePanel.Visibility == Visibility.Hidden) + else if (sidePanelBookmarks.Visibility == Visibility.Hidden) { - if (sidePanel.ActualWidth >= 50) + if (sidePanelBookmarks.ActualWidth >= 50) { - sidePanel.Visibility = Visibility.Visible; + sidePanelBookmarks.Visibility = Visibility.Visible; btnBookmarks.IsChecked = true; - _lastSidePanelWidth = sidePanelColumn.ActualWidth; + _lastSidePanelBookmarksWidth = sidePanelBookmarksColumn.ActualWidth; + } + + else + { + sidePanelBookmarksColumn.Width = new GridLength(0); + } + } + } + + private void SidePanelAnnotations_SizeChanged(object sender, + SizeChangedEventArgs e) + { + if (sidePanelAnnotations.Visibility == Visibility.Visible) + { + if (sidePanelAnnotations.ActualWidth < 50) + { + sidePanelAnnotations.Visibility = Visibility.Hidden; + sidePanelAnnotationsColumn.Width = new GridLength(0); + btnAnnotations.IsChecked = false; + _lastSidePanelAnnotationsWidth = 0; + } + + else + { + _lastSidePanelAnnotationsWidth = sidePanelAnnotationsColumn.ActualWidth; + } + } + + else if (sidePanelAnnotations.Visibility == Visibility.Hidden) + { + if (sidePanelAnnotations.ActualWidth >= 50) + { + sidePanelAnnotations.Visibility = Visibility.Visible; + btnAnnotations.IsChecked = true; + + _lastSidePanelAnnotationsWidth = sidePanelAnnotationsColumn.ActualWidth; } else { - sidePanelColumn.Width = new GridLength(0); + sidePanelAnnotationsColumn.Width = new GridLength(0); } } } @@ -485,5 +651,88 @@ private bool DictionaryControl_OnAfterExtract(bool success) } #endregion + + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, IntPtr windowTitle); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr SetWindowsHookEx(int idHook, HookHandlerDelegate lpfn, IntPtr hInstance, int threadId); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + private static extern short GetKeyState(int keyCode); + + + [DllImport("kernel32.dll")] + public static extern int GetCurrentThreadId(); + + [DllImport("user32.dll")] + static extern bool UnhookWindowsHookEx(IntPtr hInstance); + + public delegate IntPtr HookHandlerDelegate(int nCode, IntPtr wParam, IntPtr lParam); + + //Keyboard API constants + private const int WH_GETMESSAGE = 3; + private const int WM_KEYDOWN = 0x0100; + private const int WM_SYSKEYDOWN = 0x0104; + + + private const uint VK_MENU = 0x12; + private const uint VK_X = 0x58; + + //Remove message constants + private const int PM_NOREMOVE = 0x0000; + + //Variables used in the call to SetWindowsHookEx + private IntPtr hHook = IntPtr.Zero; + + private IntPtr HookCallBack(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0 || wParam.ToInt32() == PM_NOREMOVE) + { + MSG msg = (MSG)Marshal.PtrToStructure(lParam, typeof(MSG)); + if (msg.message == WM_KEYDOWN || msg.message == WM_SYSKEYDOWN) + { + if ((uint)msg.wParam == VK_X && (GetKeyState((int)VK_MENU) & 0x8000) == 0x8000) + { + if (this.IsLoaded && this.IsActive && AnnotationWebBrowserWrapper.AnnotationWebBrowser.Focused) + { + AnnotationWebBrowserWrapper.Extract(); + } + } + } + } + return CallNextHookEx(hHook, nCode, wParam, lParam); + } + + private HookHandlerDelegate hookHandlerDelegate; + + private void InstallHook() + { + IntPtr wnd = AnnotationWebBrowserWrapper.AnnotationWebBrowser.Handle; + if (wnd != IntPtr.Zero) + { + var allChildWindows = new WindowHandleInfo(wnd).GetAllChildHandles(); + wnd = FindWindowEx(wnd, IntPtr.Zero, "Shell Embedding", IntPtr.Zero); + if (wnd != IntPtr.Zero) + { + wnd = FindWindowEx(wnd, IntPtr.Zero, "Shell DocObject View", IntPtr.Zero); + if (wnd != IntPtr.Zero) + { + wnd = FindWindowEx(wnd, IntPtr.Zero, "Internet Explorer_Server", IntPtr.Zero); + if (wnd != IntPtr.Zero) + { + hookHandlerDelegate = new HookHandlerDelegate(HookCallBack); + hHook = SetWindowsHookEx(WH_GETMESSAGE, hookHandlerDelegate, (IntPtr)0, GetCurrentThreadId()); + } + } + } + } + } } } diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Inputs.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Inputs.cs index 507d182..3f4e863 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Inputs.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Inputs.cs @@ -106,6 +106,11 @@ protected override void OnKeyDown(KeyEventArgs e) e.Handled = true; break; + case PDFHotKeys.Annotate: + CreateAnnotationHighlight(); + e.Handled = true; + break; + // // PDF features diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Selection.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Selection.cs index 63d4f1d..354e228 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Selection.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Selection.cs @@ -50,6 +50,7 @@ namespace SuperMemoAssistant.Plugins.PDF.PDF.Viewer { + using SuperMemoAssistant.Extensions; using System.Diagnostics.CodeAnalysis; /// @@ -62,13 +63,12 @@ public partial class IPDFViewer #endregion - + public PDFAnnotationHighlight? CurrentAnnotationHighlight { get; set; } = null; #region Properties & Fields - Non-Public protected SelectionType CurrentSelectionTool { get; set; } = SelectionType.None; - protected PDFPageSelection SelectedPages { get; set; } protected List SelectedImageList { get; } = new List(); @@ -391,12 +391,61 @@ protected bool OnMouseMoveProcessSelection(MouseEventArgs e, invalidate = true; } + if (e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released) + { + var charIndex = GetCharIndexAtPos(pageIndex, pagePoint); + if (CurrentAnnotationHighlight == null) + { + // TODO Check if hovering over any annotationHighlights and then change the color + if (PDFElement.AnnotationHighlights.ContainsKey(pageIndex)) + { + var annotationHighlightsAtPageIndex = PDFElement.AnnotationHighlights[pageIndex]; + + foreach (PDFAnnotationHighlight annotationHighlight in annotationHighlightsAtPageIndex) + { + if (charIndex > annotationHighlight.StartIndex + && charIndex < annotationHighlight.EndIndex) + { + ChangeColorOfAnnotationHighlight(annotationHighlight); + CurrentAnnotationHighlight = annotationHighlight; + } + } + } + } + else + { + if (charIndex < CurrentAnnotationHighlight.StartIndex + || charIndex > CurrentAnnotationHighlight.EndIndex) + { + ChangeColorOfAnnotationHighlight(null); + CurrentAnnotationHighlight = null; + } + } + } + if (invalidate) InvalidateVisual(); return handled; } + public void ChangeColorOfAnnotationHighlight(PDFAnnotationHighlight? annotation) + { + ExtractHighlights.Clear(); + ImageExtractHighlights.Clear(); + RemoveHighlightFromText(); + + PDFElement.PDFExtracts.ForEach(AddPDFExtractHighlight); + PDFElement.SMExtracts.ForEach(AddSMExtractHighlight); + PDFElement.SMImgExtracts.ForEach(e => AddImgExtractHighlight(e.PageIndex, e.BoundingBox)); + PDFElement.IgnoreHighlights.ForEach(AddIgnoreHighlight); + PDFElement.AnnotationHighlights.ForEach( + alist => alist.Value.ForEach( + a => AddAnnotationHighlight(a, annotation != null && a == annotation) + ) + ); + } + protected bool OnMouseUpProcessSelection(MouseButtonEventArgs e, int pageIndex, Point pagePoint) diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.SuperMemo.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.SuperMemo.cs index 0a8719d..659a655 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.SuperMemo.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.SuperMemo.cs @@ -158,32 +158,7 @@ protected bool CreateSMExtract(double priority) // Generate extract if (contents.Count > 0) { - Save(false); - - var bookmarks = pageIndices.Select(FindBookmark) - .Where(b => b != null) - .Distinct() - .Select(b => $"({b.ToHierarchyString()})"); - var bookmarksStr = StringEx.Join(" ; ", bookmarks); - var parentEl = Svc.SM.Registry.Element[PDFElement.ElementId]; - - var templateId = imgExtracts.Count > 0 ? Config.ImageTemplate : Config.TextTemplate; - var template = Svc.SM.Registry.Template[templateId]; - - ret = Svc.SM.Registry.Element.Add( - out _, - ElemCreationFlags.CreateSubfolders, - new ElementBuilder(ElementType.Topic, - contents.ToArray()) - .WithParent(parentEl) - .WithConcept(parentEl.Concept) - .WithLayout(Config.Layout) - .WithTemplate(template) - .WithPriority(priority) - .WithReference(r => PDFElement.ConfigureSMReferences(r, bookmarks: bookmarksStr)) - .WithTitle(extractTitle) - .DoNotDisplay() - ); + ret = CreateAndAddSMExtract(contents, extractTitle, pageIndices, imgExtracts.Count > 0, priority); Window.GetWindow(this)?.Activate(); @@ -222,6 +197,47 @@ protected bool CreateSMExtract(double priority) return ret; } + public bool CreateAndAddSMExtract( + List contents, + string extractTitle, + HashSet pageIndices, + bool useImageTemplate = false, + double? priority = null) + { + if (priority == null) + { + priority = Config.PDFExtractPriority; + } + Save(false); + + var bookmarks = pageIndices.Select(FindBookmark) + .Where(b => b != null) + .Distinct() + .Select(b => $"({b.ToHierarchyString()})"); + var bookmarksStr = StringEx.Join(" ; ", bookmarks); + var parentEl = Svc.SM.Registry.Element[PDFElement.ElementId]; + + var templateId = useImageTemplate ? Config.ImageTemplate : Config.TextTemplate; + var template = Svc.SM.Registry.Template[templateId]; + + var ret = Svc.SM.Registry.Element.Add( + out _, + ElemCreationFlags.CreateSubfolders, + new ElementBuilder(ElementType.Topic, + contents.ToArray()) + .WithParent(parentEl) + .WithConcept(parentEl.Concept) + .WithLayout(Config.Layout) + .WithTemplate(template) + .WithPriority((double)priority) + .WithReference(r => PDFElement.ConfigureSMReferences(r, bookmarks: bookmarksStr)) + .WithTitle(extractTitle) + .DoNotDisplay() + ); + + return ret; + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("AsyncUsage", "AsyncFixer03:Avoid fire & forget async void methods", Justification = "")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] @@ -356,6 +372,31 @@ protected bool CreateIgnoreHighlight() return true; } + protected bool CreateAnnotationHighlight() + { + if (IsTextSelectionValid(out _) == false) + return false; + + foreach (var selInfo in SelectInfos) + { + var count = 0; + foreach (var annotationHighlightsAtPageIndex in PDFElement.AnnotationHighlights) + { + foreach (PDFAnnotationHighlight a in annotationHighlightsAtPageIndex.Value) + { + count = (a.AnnotationId >= count) ? a.AnnotationId + 1 : count; + } + } + var annotationHighlight = PDFAnnotationHighlight.Create(selInfo, count, GetSelectedTextAsHtml()); + PDFElement.AddAnnotationHighlight(annotationHighlight); + AddAnnotationHighlight(annotationHighlight); + } + + DeselectText(); + + return true; + } + // // Highlights @@ -419,6 +460,19 @@ protected void AddIgnoreHighlight(PDFTextExtract extract) AddHighlight(extract, Config.IgnoreHighlightColor); } + protected void AddAnnotationHighlight(PDFTextExtract extract) + => AddAnnotationHighlight(extract, false); + + protected void AddAnnotationHighlight(PDFTextExtract extract, bool isFocused) + { + AddHighlight( + extract, + isFocused + ? Config.FocusedAnnotationHighlightColor + : Config.AnnotationHighlightColor + ); + } + protected void AddHighlight(PDFTextExtract extract, System.Windows.Media.Color highlightColor) { diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Utils.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Utils.cs index 1efe811..b316d83 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Utils.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.Utils.cs @@ -98,7 +98,7 @@ public void ShowDictionaryPopup() return; */ - var pageIdx = SelectInfo.StartPage; + var pageIdx = SelectInfo.StartPage; var startIdx = SelectInfo.StartIndex; var textInfos = Document.Pages[pageIdx].Text.GetTextInfo(startIdx, text.Length); @@ -133,7 +133,7 @@ public void ShowDictionaryPopup() textInfos.Rects.Last().top)); DictionaryPopup.HorizontalOffset = pagePt.X; - DictionaryPopup.VerticalOffset = pagePt.Y; + DictionaryPopup.VerticalOffset = pagePt.Y; DictionaryPopup.DataContext = new PendingEntryResult(cts, entryResultTask, dict); @@ -141,8 +141,8 @@ public void ShowDictionaryPopup() } public async Task LookupWordEntryAsync(RemoteCancellationToken ct, - string word, - IDictionaryService dict) + string word, + IDictionaryService dict) { var lemmas = await dict.LookupLemma(ct, word, (Config.PDFDictionary ?? dict.DefaultDictionary).GetLanguageId()); @@ -335,7 +335,7 @@ protected bool IsEndOfSelectionInScreen() return ClientRect.Contains(topLeftPt); } - public override void ScrollToPoint(int pageIndex, + public override void ScrollToPoint(int pageIndex, Point pagePoint) { int count = Document?.Pages.Count ?? 0; @@ -352,6 +352,21 @@ public override void ScrollToPoint(int pageIndex, pagePoint); } + public void ScrollToAnnotationHighlight(PDFAnnotationHighlight annotation) + { + var point = GetCharPoint(annotation.StartPage, + annotation.StartIndex); + if (PointInPage(point) == -1) + { + ScrollToChar(annotation.EndPage, + annotation.EndIndex); + + var scrollY = -_viewport.Height / 2 - _autoScrollPosition.Y; + + SetVerticalOffset(scrollY); + } + } + protected void ScrollToEndOfSelection() { ScrollToChar(SelectInfo.EndPage, diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.cs index 0f93959..9317e90 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/IPDFViewer.cs @@ -132,6 +132,7 @@ protected override void OnDocumentLoaded(EventArgs ev) PDFElement.SMExtracts.ForEach(AddSMExtractHighlight); PDFElement.SMImgExtracts.ForEach(e => AddImgExtractHighlight(e.PageIndex, e.BoundingBox)); PDFElement.IgnoreHighlights.ForEach(AddIgnoreHighlight); + PDFElement.AnnotationHighlights.ForEach(e => e.Value.ForEach(AddAnnotationHighlight)); GenerateOutOfExtractHighlights(); diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/WebBrowserWrapper/PDFAnnotationWebBrowserWrapper.cs b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/WebBrowserWrapper/PDFAnnotationWebBrowserWrapper.cs new file mode 100644 index 0000000..ae772e8 --- /dev/null +++ b/src/SuperMemoAssistant.Plugins.PDF/PDF/Viewer/WebBrowserWrapper/PDFAnnotationWebBrowserWrapper.cs @@ -0,0 +1,168 @@ +using SuperMemoAssistant.Extensions; +using SuperMemoAssistant.Interop; +using SuperMemoAssistant.Interop.SuperMemo.Content.Contents; +using SuperMemoAssistant.Plugins.PDF.Models; +using SuperMemoAssistant.Services; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Windows; + +namespace SuperMemoAssistant.Plugins.PDF.PDF.Viewer.WebBrowserWrapper +{ + public class PDFAnnotationWebBrowserWrapper + { + private IPDFViewer PDFViewer { get; set; } + private int? SelectedAnnotationId { get; set; } = null; + public System.Windows.Forms.WebBrowser AnnotationWebBrowser { get; set; } + public PDFAnnotationWebBrowserWrapper(System.Windows.Forms.Integration.WindowsFormsHost wfHost, IPDFViewer pdfViewer) + { + PDFViewer = pdfViewer; + AnnotationWebBrowser = new System.Windows.Forms.WebBrowser(); + wfHost.Child = AnnotationWebBrowser; + AnnotationWebBrowser.DocumentCompleted += WebBrowserLoadCompletedEventHandler; + var configDir = SMAFileSystem.ConfigDir.Combine("SuperMemoAssistant.Plugins.PDF"); + AnnotationWebBrowser.Url = new Uri($@"{configDir}/annotationSidePanel.html"); + AnnotationWebBrowser.WebBrowserShortcutsEnabled = false; + AnnotationWebBrowser.ObjectForScripting = this; + } + + private void WebBrowserLoadCompletedEventHandler(object sender, + EventArgs e) + { + RefreshAnnotations(); + PDFViewer.PDFElement.OnAnnotationAdded += + AnnotationHighlights_AnnotationAdded; + } + + + public void Extract() + { + AnnotationWebBrowser.Document.InvokeScript("handleExtract"); + } + + public void RefreshAnnotations() + { + ClearAnnotations(); + PDFViewer.PDFElement.AnnotationHighlights.ForEach( + alist => alist.Value.ForEach( + a => InsertAnnotation(a) + ) + ); + } + + public void ClearAnnotations() + { + AnnotationWebBrowser.Document.InvokeScript("clearAnnotations"); + } + + public void InsertAnnotation(PDFAnnotationHighlight annotationHighlight) + { + var innerHtml = annotationHighlight.HtmlContent; + var annotationId = annotationHighlight.AnnotationId; + var annotationSortingKey = annotationHighlight.GetSortingKey(); + AnnotationWebBrowser.Document.InvokeScript("insertAnnotation", new object[] {annotationId, annotationSortingKey, innerHtml}); + ScrollToAnnotation(annotationHighlight); + } + + public void ScrollToAnnotation(PDFAnnotationHighlight annotationHighlight) + { + AnnotationWebBrowser.Document.InvokeScript("scrollToAnnotation", new object[] {annotationHighlight.AnnotationId}); + } + + public void AnnotationHighlights_AnnotationAdded(object sender, + AnnotationAddedEventArgs e) + { + InsertAnnotation(e.NewItem); + PDFViewer.PDFElement.IsChanged = true; + PDFViewer.PDFElement.Save(); + } + + public void Annotation_HandleExtract(string extractHtml) + { + var hasTextSelection = string.IsNullOrWhiteSpace(extractHtml) == false; + + if (!hasTextSelection || SelectedAnnotationId == null) + return; + + var annotationHighlight = GetAnnotationHighlightFromId((int)SelectedAnnotationId); + if (annotationHighlight == null) + return; + + var parentEl = Svc.SM.Registry.Element[PDFViewer.PDFElement.ElementId]; + + var pageIndices = new HashSet(); + for (int p = annotationHighlight.StartPage; p <= annotationHighlight.EndPage; p++) + pageIndices.Add(p); + + var titleString = $"{parentEl.Title} -- Annotation extract:"; + var pageString = "p" + string.Join(", p", pageIndices.Select(p => p + 1)); + var extractTitle = $"{titleString} {extractHtml} from Annotation #{SelectedAnnotationId} from {pageString}"; + var contents = new List(); + contents.Add(new TextContent(true, extractHtml)); + + PDFViewer.CreateAndAddSMExtract(contents, extractTitle, pageIndices); + + Window.GetWindow(PDFViewer)?.Activate(); + } + + public void Annotation_OnFocus(int annotationId) { + SelectedAnnotationId = annotationId; + } + + public void Annotation_OnClick(int annotationId) => ScrollToAnnotationWithId(annotationId); + + public void Annotation_OnAfterUpdate() => UpdateAnnotationHighlights(); + + public PDFAnnotationHighlight? GetAnnotationHighlightFromId(int annotationId) + { + foreach (var annotationsOnPage in PDFViewer.PDFElement.AnnotationHighlights) + { + foreach (PDFAnnotationHighlight annotation in annotationsOnPage.Value) + { + if (annotation.AnnotationId == annotationId) + return annotation; + } + } + return null; + } + + public void UpdateAnnotationHighlights() + { + PDFViewer.PDFElement.IsChanged = true; + foreach (var annotationsOnPage in PDFViewer.PDFElement.AnnotationHighlights) + { + foreach (PDFAnnotationHighlight annotation in annotationsOnPage.Value) + { + annotation.HtmlContent = + GetHTMLContentForAnnotationId(annotation.AnnotationId) + ?? annotation.HtmlContent; + } + } + PDFViewer.PDFElement.Save(); + } + + public void ScrollToAnnotationWithId(int annotationId) + { + foreach (var annotationsOnPage in PDFViewer.PDFElement.AnnotationHighlights) + { + foreach (PDFAnnotationHighlight annotation in annotationsOnPage.Value) + { + if (annotation.AnnotationId == annotationId) + { + PDFViewer.ScrollToAnnotationHighlight(annotation); + PDFViewer.ChangeColorOfAnnotationHighlight(annotation); + } + } + } + } + + public string GetHTMLContentForAnnotationId(int annotationId) + { + var document = (mshtml.HTMLDocument)AnnotationWebBrowser.Document.DomDocument; + var element = document.getElementById("annotation" + annotationId.ToString()); + return element?.innerHTML; + } + } +} diff --git a/src/SuperMemoAssistant.Plugins.PDF/PDFHotKeys.cs b/src/SuperMemoAssistant.Plugins.PDF/PDFHotKeys.cs index dee7554..04264ff 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/PDFHotKeys.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/PDFHotKeys.cs @@ -46,6 +46,7 @@ internal static class PDFHotKeys public const string ExtractSM = "ExtractSM"; public const string ExtractSMWithPriority = "ExtractSMWithPriority"; public const string MarkIgnore = "MarkIgnore"; + public const string Annotate = "Annotate"; public const string ShowDictionary = "ShowDictionary"; public const string GoToPage = "GoToPage"; public const string SMLearn = "SMLearn"; @@ -61,6 +62,7 @@ internal static class PDFHotKeys public const string SMPrevSibling = "SMPrevSibling"; public const string SMNextSibling = "SMNextSibling"; public const string UIShowOptions = "UIShowOptions"; + public const string UIToggleAnnotations = "UIToggleAnnotations"; public const string UIToggleBookmarks = "UIToggleBookmarks"; public const string UIFocusViewer = "UIFocusViewer"; public const string UIFocusBookmarks = "UIFocusBookmarks"; @@ -104,6 +106,10 @@ public static void RegisterHotKeys() "Mark text as ignored", new HotKey(Key.I, KeyModifiers.CtrlShift) ) + .RegisterLocal(Annotate, + "Create annotation for selected text", + new HotKey(Key.A, KeyModifiers.CtrlShift) + ) // // PDF features @@ -176,6 +182,10 @@ public static void RegisterHotKeys() "Show options", new HotKey(Key.O, KeyModifiers.Ctrl) ) + .RegisterLocal(UIToggleAnnotations, + "Toggle annotations", + new HotKey(Key.A, KeyModifiers.Ctrl) + ) .RegisterLocal(UIToggleBookmarks, "Toggle bookmarks", new HotKey(Key.B, KeyModifiers.Ctrl) diff --git a/src/SuperMemoAssistant.Plugins.PDF/SuperMemoAssistant.Plugins.PDF.csproj b/src/SuperMemoAssistant.Plugins.PDF/SuperMemoAssistant.Plugins.PDF.csproj index 1265db3..8399aa7 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/SuperMemoAssistant.Plugins.PDF.csproj +++ b/src/SuperMemoAssistant.Plugins.PDF/SuperMemoAssistant.Plugins.PDF.csproj @@ -39,12 +39,15 @@ + + + @@ -124,7 +127,10 @@ $(PkgPdfium_Net_SDK)\lib\net472\Patagames.Pdf.dll - + + + + diff --git a/src/SuperMemoAssistant.Plugins.PDF/Utils/Web/HtmlBuilder.cs b/src/SuperMemoAssistant.Plugins.PDF/Utils/Web/HtmlBuilder.cs index 8d08ee1..9faa9c2 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/Utils/Web/HtmlBuilder.cs +++ b/src/SuperMemoAssistant.Plugins.PDF/Utils/Web/HtmlBuilder.cs @@ -521,6 +521,15 @@ private void GenerateExtractSpans() SMConst.Stylesheet.IgnoreColor, pageSpanDict); + foreach (var annotationsOnPage in PdfElement.AnnotationHighlights) + foreach (var annotationHighlight in annotationsOnPage.Value) + SplitExtractByPage(annotationHighlight, + Color.FromArgb(90, + 100, + 255, + 100), + pageSpanDict); + foreach (var pageSpan in pageSpanDict) GenerateExtractSpans(pageSpan.Key, pageSpan.Value diff --git a/src/SuperMemoAssistant.Plugins.PDF/app.manifest b/src/SuperMemoAssistant.Plugins.PDF/app.manifest index 9e0851b..b692f73 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/app.manifest +++ b/src/SuperMemoAssistant.Plugins.PDF/app.manifest @@ -16,7 +16,7 @@ Remove this element if your application requires this virtualization for backwards compatibility. --> - + diff --git a/src/SuperMemoAssistant.Plugins.PDF/version.json b/src/SuperMemoAssistant.Plugins.PDF/version.json index c62f9b7..4dc71a1 100644 --- a/src/SuperMemoAssistant.Plugins.PDF/version.json +++ b/src/SuperMemoAssistant.Plugins.PDF/version.json @@ -1,3 +1,3 @@ { - "version": "2.1.0-beta" + "version": "2.1.0" } \ No newline at end of file