Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .markdownlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ default: true
MD013: false
MD033: false
MD041: false
MD060: false

MD007:
indent: 4
Expand Down
2 changes: 0 additions & 2 deletions CLA.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ one You submitted before**. Code reaches a project by more routes than a Pull
Request, and a signature that only looked forward would leave those earlier
routes unaccounted for.


**"Project Owner"** means Mario Hidalgo G. (rgdevment), the copyright holder
and maintainer of CopyPaste.

Expand Down Expand Up @@ -61,7 +60,6 @@ above. Where such rights cannot be waived, You agree not to enforce them in a
way that blocks those uses. This does not touch attribution: section 6 commits
the Project Owner to preserving Your authorship.


## 3. Patent licence

You grant the Project Owner, and to recipients of software distributed by the
Expand Down
2 changes: 2 additions & 0 deletions app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@
"pasteDestinationUnavailable": "Paste was cancelled because the original destination could not be restored. Open CopyPaste with its keyboard shortcut and try again.",
"plainPasteItemUnavailable": "The hovered, selected, or first item cannot be pasted as plain text.",
"plainClipboardUnavailable": "There is no text on the clipboard. Copy some text first, then use plain-text paste again.",
"clipboardWriteFailed": "The item could not be placed on the clipboard because another app is holding it. Try again in a moment.",
"pasteTargetElevated": "The destination app runs as administrator, so Windows blocks the simulated paste. Run CopyPaste as administrator too, or press Ctrl+V yourself.",
"hotkeyRegistrationFailed": "The shortcut {shortcut} could not be registered. It may already be in use by the system or another app.",
"@hotkeyRegistrationFailed": {
"placeholders": { "shortcut": { "type": "String" } }
Expand Down
2 changes: 2 additions & 0 deletions app/lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@
"pasteDestinationUnavailable": "Se canceló el pegado porque no se pudo restaurar el destino original. Abre CopyPaste con su atajo de teclado e inténtalo nuevamente.",
"plainPasteItemUnavailable": "El elemento bajo el cursor, el seleccionado o el primero no se puede pegar como texto plano.",
"plainClipboardUnavailable": "No hay texto en el portapapeles. Copia primero un texto y vuelve a usar el pegado como texto plano.",
"clipboardWriteFailed": "No se pudo copiar el elemento al portapapeles porque otra aplicación lo tiene retenido. Vuelve a intentarlo en un momento.",
"pasteTargetElevated": "La aplicación de destino se ejecuta como administrador, así que Windows bloquea el pegado simulado. Ejecuta CopyPaste también como administrador o pulsa Ctrl+V tú mismo.",
"hotkeyRegistrationFailed": "No se pudo registrar el atajo {shortcut}. Es posible que el sistema u otra aplicación ya lo esté usando.",
"@hotkeyRegistrationFailed": {
"placeholders": { "shortcut": { "type": "String" } }
Expand Down
12 changes: 12 additions & 0 deletions app/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,18 @@ abstract class AppLocalizations {
/// **'There is no text on the clipboard. Copy some text first, then use plain-text paste again.'**
String get plainClipboardUnavailable;

/// No description provided for @clipboardWriteFailed.
///
/// In en, this message translates to:
/// **'The item could not be placed on the clipboard because another app is holding it. Try again in a moment.'**
String get clipboardWriteFailed;

/// No description provided for @pasteTargetElevated.
///
/// In en, this message translates to:
/// **'The destination app runs as administrator, so Windows blocks the simulated paste. Run CopyPaste as administrator too, or press Ctrl+V yourself.'**
String get pasteTargetElevated;

/// No description provided for @hotkeyRegistrationFailed.
///
/// In en, this message translates to:
Expand Down
8 changes: 8 additions & 0 deletions app/lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,14 @@ class AppLocalizationsEn extends AppLocalizations {
String get plainClipboardUnavailable =>
'There is no text on the clipboard. Copy some text first, then use plain-text paste again.';

@override
String get clipboardWriteFailed =>
'The item could not be placed on the clipboard because another app is holding it. Try again in a moment.';

@override
String get pasteTargetElevated =>
'The destination app runs as administrator, so Windows blocks the simulated paste. Run CopyPaste as administrator too, or press Ctrl+V yourself.';

@override
String hotkeyRegistrationFailed(String shortcut) {
return 'The shortcut $shortcut could not be registered. It may already be in use by the system or another app.';
Expand Down
8 changes: 8 additions & 0 deletions app/lib/l10n/app_localizations_es.dart
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,14 @@ class AppLocalizationsEs extends AppLocalizations {
String get plainClipboardUnavailable =>
'No hay texto en el portapapeles. Copia primero un texto y vuelve a usar el pegado como texto plano.';

@override
String get clipboardWriteFailed =>
'No se pudo copiar el elemento al portapapeles porque otra aplicación lo tiene retenido. Vuelve a intentarlo en un momento.';

@override
String get pasteTargetElevated =>
'La aplicación de destino se ejecuta como administrador, así que Windows bloquea el pegado simulado. Ejecuta CopyPaste también como administrador o pulsa Ctrl+V tú mismo.';

@override
String hotkeyRegistrationFailed(String shortcut) {
return 'No se pudo registrar el atajo $shortcut. Es posible que el sistema u otra aplicación ya lo esté usando.';
Expand Down
30 changes: 25 additions & 5 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ class _CopyPasteAppState extends State<CopyPasteApp>
_config = widget.config;
_appWindow = AppWindow(
onVisibilityChanged: _onWindowVisibilityChanged,
showInTaskbar: false,
popupWidth: _config.popupWidth.toDouble(),
popupHeight: _config.popupHeight.toDouble(),
rememberPositionEnabled: () => _config.rememberWindowPosition,
Expand Down Expand Up @@ -837,7 +836,12 @@ class _CopyPasteAppState extends State<CopyPasteApp>
widget.clipboardService.notifyDirectPasteInitiated(text);
final written = await ClipboardWriter.setText(text, plainText: true);
if (!written) {
AppLogger.warn('Plain-text paste aborted: clipboard write failed');
if (!panelWasVisible) pasteFocusManager.clear();
_showShellNotice(
(l) => l.clipboardWriteFailed,
revealWhenHidden: !panelWasVisible,
);
return;
}

Expand All @@ -853,10 +857,12 @@ class _CopyPasteAppState extends State<CopyPasteApp>
pasteFocusManager.clear();
return;
}
// With the panel open the destination lost activation and has to settle
// exactly like the item paste does; only the hidden path can skip it.
final response = await pasteFocusManager.restoreAndPaste(
delayBeforeFocusMs: 0,
delayBeforeFocusMs: panelWasVisible ? _config.delayBeforeFocusMs : 0,
maxFocusVerifyAttempts: _config.maxFocusVerifyAttempts,
delayBeforePasteMs: 0,
delayBeforePasteMs: panelWasVisible ? _config.delayBeforePasteMs : 0,
);
if (!response.success) _reportPasteFailure(response);
} on PlatformException catch (e) {
Expand Down Expand Up @@ -945,6 +951,7 @@ class _CopyPasteAppState extends State<CopyPasteApp>
bool plainText = false,
}) async {
if (_itemPasteInProgress ||
_directPlainPasteInProgress ||
(item.isFileBasedType && !item.isFileAvailable())) {
return;
}
Expand All @@ -963,7 +970,12 @@ class _CopyPasteAppState extends State<CopyPasteApp>
metadata: item.metadata,
plainText: plainText,
);
if (!ok) return;
if (!ok) {
AppLogger.warn('Item paste aborted: clipboard write failed');
// The destination is intentionally kept so the user can retry.
_showShellNotice((l) => l.clipboardWriteFailed);
return;
}
await _appWindow.hide();
if (!Platform.isWindows && !await _waitForShortcutModifiersReleased()) {
_focusManager.clear();
Expand Down Expand Up @@ -1009,6 +1021,10 @@ class _CopyPasteAppState extends State<CopyPasteApp>
);
return;
}
if (response.errorCode == 'targetElevated') {
_showShellNotice((l) => l.pasteTargetElevated, revealWhenHidden: true);
return;
}
_showShellNotice(
(l) => l.pasteDestinationUnavailable,
revealWhenHidden: true,
Expand Down Expand Up @@ -1046,7 +1062,11 @@ class _CopyPasteAppState extends State<CopyPasteApp>
content: item.content,
metadata: item.metadata,
);
if (!ok) return;
if (!ok) {
AppLogger.warn('Copy aborted: clipboard write failed');
_showShellNotice((l) => l.clipboardWriteFailed);
return;
}
await widget.clipboardService.recordCopy(item.id);
if (!mounted) return;
final ctx = _navigatorKey.currentContext;
Expand Down
50 changes: 23 additions & 27 deletions app/lib/shell/app_window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ class _Win32Pos {
class AppWindow {
AppWindow({
this.onVisibilityChanged,
this.showInTaskbar = true,
double popupWidth = 360,
double popupHeight = 500,
this.rememberPositionEnabled,
Expand All @@ -150,8 +149,6 @@ class AppWindow {
}) : _popupWidth = popupWidth,
_popupHeight = popupHeight;

bool showInTaskbar;

static const double _settingsWidth = 820;
static const double _settingsHeight = 680;

Expand All @@ -177,7 +174,6 @@ class AppWindow {
Future<void> init({bool startVisible = false}) async {
AppLogger.info(
'AppWindow.init: startVisible=$startVisible, '
'showInTaskbar=$showInTaskbar, '
'size=${_popupWidth}x$_popupHeight',
);
try {
Expand Down Expand Up @@ -217,8 +213,15 @@ class AppWindow {
await windowManager.setResizable(false);
await windowManager.setMaximizable(false);
await windowManager.setPreventClose(true);
final inTaskbar = showInTaskbar && Platform.isWindows;
await windowManager.setSkipTaskbar(!inTaskbar);
await windowManager.setSkipTaskbar(true);
if (Platform.isMacOS) {
// Without this, opening the panel over a full-screen app switches Space,
// and the animation blows past the paste focus budget.
await windowManager.setVisibleOnAllWorkspaces(
true,
visibleOnFullScreen: true,
);
}
if (Platform.isWindows || Platform.isMacOS) {
await windowManager.setBackgroundColor(const Color(0x00000000));
AppLogger.info('_configureWindow: applying initial effect');
Expand All @@ -228,9 +231,6 @@ class AppWindow {
AppLogger.info('_configureWindow: centering and focusing');
await windowManager.center();
await windowManager.focus();
} else if (inTaskbar) {
AppLogger.info('_configureWindow: minimizing to taskbar');
await windowManager.minimize();
} else {
AppLogger.info('_configureWindow: hiding window');
await windowManager.hide();
Expand Down Expand Up @@ -696,23 +696,19 @@ class AppWindow {
if (!_visible) return;
_visible = false;
await _captureCurrentPosition();
if (showInTaskbar && Platform.isWindows) {
await windowManager.minimize();
} else {
Future<bool>? unmappedFuture;
if (Platform.isLinux) {
unmappedFuture = LinuxShell.awaitEvent(
'unmapped',
timeout: const Duration(milliseconds: 300),
);
}
await windowManager.hide();
if (!Platform.isMacOS) {
await windowManager.setSkipTaskbar(true);
}
if (unmappedFuture != null) {
await unmappedFuture;
}
Future<bool>? unmappedFuture;
if (Platform.isLinux) {
unmappedFuture = LinuxShell.awaitEvent(
'unmapped',
timeout: const Duration(milliseconds: 300),
);
}
await windowManager.hide();
if (!Platform.isMacOS) {
await windowManager.setSkipTaskbar(true);
}
if (unmappedFuture != null) {
await unmappedFuture;
}
onVisibilityChanged?.call(false);
}
Expand Down Expand Up @@ -807,7 +803,7 @@ class AppWindow {
Future<void> exitGateMode() async {
_gateMode = false;
await windowManager.setAlwaysOnTop(true);
await windowManager.setSkipTaskbar(!(showInTaskbar && Platform.isWindows));
await windowManager.setSkipTaskbar(true);
await windowManager.setMinimumSize(Size(_popupWidth, 400));
await windowManager.setMaximumSize(Size(_popupWidth, 900));
await windowManager.setSize(Size(_popupWidth, _popupHeight));
Expand Down
Loading
Loading