Files
mmd-pdf/Shell/FullScreen.cs
T

226 lines
13 KiB
C#

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace KillerPDF
{
public partial class MainWindow
{
private bool _fullScreen;
private GridLength _fsTitleRow, _fsFooterRow, _fsSidebarCol, _fsSplitterCol;
private double _fsSidebarMin;
private WindowState _fsPrevState;
private bool _fsPrevTopmost;
private ResizeMode _fsPrevResize;
private double _fsPrevLeft, _fsPrevTop, _fsPrevW, _fsPrevH;
private bool _fsAnimating;
// F11 distraction-free mode: hides all chrome (title bar, toolbar, tab strip, sidebar, footer) and
// grows the window over the whole monitor so just the document pane fills the screen on a dark-gray
// backdrop. F11 or Esc exits. The switch happens under a black cross-fade so the resize never jumps.
private void ToggleFullScreen()
{
if (_fsAnimating) return; // ignore re-presses mid-transition
_fsAnimating = true;
bool entering = !_fullScreen;
var cover = new Border { Background = Brushes.Black, Opacity = 0, IsHitTestVisible = false };
Grid.SetRow(cover, 0);
Grid.SetRowSpan(cover, RootClipGrid.RowDefinitions.Count);
Panel.SetZIndex(cover, 99998);
RootClipGrid.Children.Add(cover);
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150))
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } };
fadeIn.Completed += (_, _2) =>
{
ApplyFullScreen(entering);
// Reveal only after the resize/relayout has settled, so no edge of old layout flashes through.
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200))
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
fadeOut.Completed += (_, _3) =>
{
RootClipGrid.Children.Remove(cover);
_fsAnimating = false;
if (entering) ShowFullScreenHint();
};
cover.BeginAnimation(UIElement.OpacityProperty, fadeOut);
}));
};
cover.BeginAnimation(UIElement.OpacityProperty, fadeIn);
}
private void ApplyFullScreen(bool entering)
{
// The split panes keep their RATIO across the size jump - OnSplitHostResized handles
// every non-interactive resize (full screen included), so nothing extra is needed here.
_fullScreen = entering;
var v = entering ? Visibility.Collapsed : Visibility.Visible;
TitleBarBorder.Visibility = v;
// Exit must respect the user's hide-toolbar setting (#215), not blanket-restore it.
ToolbarRowBorder.Visibility = entering || _toolbarHidden ? Visibility.Collapsed : Visibility.Visible;
// BOTH panes - full screen hides every strip, not just the focused pane's.
Viewer.TabStripBorderCtl.Visibility = v;
ViewerB.TabStripBorderCtl.Visibility = v;
FooterBorder.Visibility = v;
SidebarOuterGrid.Visibility = v;
SidebarToggleStrip.Visibility = v;
SidebarSplitter.Visibility = v;
// The document pane is a lifted card in normal use (RadCard corners, an 8px inset on
// its outer side, PaneShadow). Full screen wants the canvas edge to edge, so the card
// treatment comes off entirely and goes back on exit - otherwise a rounded corner and
// an 8px strip of window background sit inside a "full screen" view.
// The MARGIN is on the PdfViewer control, not on the card border inside it - the
// control is what the layout positions.
// SplitHost, not Viewer - the inset to the window edge belongs to the host that holds
// both panes. See the note in ApplySidebarSide.
SplitHost.Margin = entering ? new Thickness(0) : DocPaneInsetMargin();
DocPaneBorder.CornerRadius = entering ? new CornerRadius(0)
: (CornerRadius)FindResource("RadCard");
DocPaneBorder.BorderThickness = new Thickness(entering ? 0 : 1);
// The shadow caster is inside the control now, so it needs no margin of its own -
// hiding it is still right, so nothing casts onto a full-screen canvas.
DocPaneShadow.Visibility = v;
if (entering)
{
_fsTitleRow = RootClipGrid.RowDefinitions[0].Height;
_fsFooterRow = RootClipGrid.RowDefinitions[4].Height;
_fsSidebarCol = _sidebarCol.Width;
_fsSidebarMin = _sidebarCol.MinWidth;
_fsSplitterCol = MainContentGrid.ColumnDefinitions[1].Width;
RootClipGrid.RowDefinitions[0].Height = new GridLength(0);
RootClipGrid.RowDefinitions[4].Height = new GridLength(0);
// Collapse the ACTUAL sidebar column (_sidebarCol), which ApplySidebarSide repoints to DocCol
// when the sidebar is on the right. MinWidth must drop to 0 too (it floors the width to 24
// otherwise) and the splitter column (col 1) collapses, so the document fills the whole screen
// with no leftover strip on either side, regardless of side or sidebar width.
_sidebarCol.MinWidth = 0;
_sidebarCol.Width = new GridLength(0);
MainContentGrid.ColumnDefinitions[1].Width = new GridLength(0);
DocPaneBorder.Background = new SolidColorBrush(Color.FromRgb(0x26, 0x26, 0x26)); // dark-gray backdrop
// Cover the whole monitor with explicit bounds. A maximized window is clamped to the work
// area (taskbar stays visible), so instead we go Normal, size to the full monitor rect, and
// set Topmost so the window sits above the always-on-top taskbar - true full screen.
_fsPrevState = WindowState;
_fsPrevTopmost = Topmost;
_fsPrevResize = ResizeMode;
_fsPrevLeft = Left; _fsPrevTop = Top; _fsPrevW = Width; _fsPrevH = Height;
// Read the target monitor while still on it (before any WindowState change). Set the target
// bounds FIRST, then drop to Normal: WPF restores to the just-set bounds, so the window lands
// straight on this monitor instead of momentarily restoring to its old normal rect on another
// screen (the "flash to another monitor"). Re-apply bounds after Normal to be certain.
// Paint the window background black for the grow. When the window jumps from its small
// rect to the full monitor, the newly-exposed area is filled by the window background
// until the black cover re-lays-out over it. Black (instead of the gray BgDark) makes
// that exposed edge match the cover, so entering no longer flashes a gray border the way
// it did - shrinking on exit exposes nothing, which is why exit already looked clean.
// Restored to BgDark on exit.
Background = Brushes.Black;
var b = CurrentMonitorBoundsDip();
Topmost = true;
// #215: Topmost exists only to cover the always-on-top taskbar while KillerPDF is
// the ACTIVE window. Held unconditionally, it sat over every other program the user
// switched to. Yield it on deactivate, take it back on return - browser behavior.
Deactivated += FsYieldTopmost;
Activated += FsReassertTopmost;
ResizeMode = ResizeMode.NoResize;
Left = b.Left; Top = b.Top; Width = b.Width; Height = b.Height;
if (WindowState == WindowState.Maximized) WindowState = WindowState.Normal;
Left = b.Left; Top = b.Top; Width = b.Width; Height = b.Height;
}
else
{
RootClipGrid.RowDefinitions[0].Height = _fsTitleRow;
RootClipGrid.RowDefinitions[4].Height = _fsFooterRow;
_sidebarCol.MinWidth = _fsSidebarMin;
_sidebarCol.Width = _fsSidebarCol;
MainContentGrid.ColumnDefinitions[1].Width = _fsSplitterCol;
DocPaneBorder.SetResourceReference(Border.BackgroundProperty, "BgCanvas");
SetResourceReference(BackgroundProperty, "SurfaceBrush"); // undo the black grow-backdrop
// Drop topmost and restore the pre-full-screen window placement. Restore the normal bounds
// first (so WPF's remembered restore rect is correct) then re-maximize if it was maximized.
Deactivated -= FsYieldTopmost;
Activated -= FsReassertTopmost;
Topmost = _fsPrevTopmost;
ResizeMode = _fsPrevResize;
WindowState = WindowState.Normal;
Left = _fsPrevLeft; Top = _fsPrevTop; Width = _fsPrevW; Height = _fsPrevH;
if (_fsPrevState == WindowState.Maximized) WindowState = WindowState.Maximized;
}
// Re-apply the frame treatment: squared (no border, square corners, full clip) in full screen,
// back to the floating border on exit. _fullScreen is already set, so UpdateWindowChrome reads it.
UpdateWindowChrome();
}
// #215: full-screen topmost is active-window-only; see the enter branch above.
private void FsYieldTopmost(object? sender, EventArgs e) => Topmost = false;
private void FsReassertTopmost(object? sender, EventArgs e) => Topmost = true;
// Full bounds (taskbar included) of the monitor the window is currently on, in WPF device-independent
// units. MonitorFromWindow/GetMonitorInfo/MONITORINFO/RECT are declared in WindowChrome.cs (same class).
private Rect CurrentMonitorBoundsDip()
{
var hwnd = new WindowInteropHelper(this).Handle;
IntPtr mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var info = new MONITORINFO { cbSize = Marshal.SizeOf(typeof(MONITORINFO)) };
GetMonitorInfo(mon, ref info);
var r = info.rcMonitor;
var dpi = VisualTreeHelper.GetDpi(this);
return new Rect(r.left / dpi.DpiScaleX, r.top / dpi.DpiScaleY,
(r.right - r.left) / dpi.DpiScaleX, (r.bottom - r.top) / dpi.DpiScaleY);
}
// Chrome-style toast: fades in near the top, holds, then fades out and removes itself.
private void ShowFullScreenHint()
{
var toast = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0xE0, 0x1c, 0x1c, 0x1c)),
CornerRadius = new CornerRadius(7),
Padding = new Thickness(18, 9, 18, 9),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 44, 0, 0),
Opacity = 0,
IsHitTestVisible = false,
Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 12, ShadowDepth = 2, Opacity = 0.5 },
Child = new TextBlock
{
Text = Loc("Str_FullScreen_Hint"), Foreground = Brushes.White, FontSize = 13,
FontFamily = UiKit.UiFont
}
};
Grid.SetRow(toast, 0);
Grid.SetRowSpan(toast, RootClipGrid.RowDefinitions.Count);
Panel.SetZIndex(toast, 99999);
RootClipGrid.Children.Add(toast);
toast.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
var t = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2.2) };
t.Tick += (_, _2) =>
{
t.Stop();
var fade = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(400));
fade.Completed += (_, _3) => RootClipGrid.Children.Remove(toast);
toast.BeginAnimation(UIElement.OpacityProperty, fade);
};
t.Start();
}
}
}