Copier-coller a la souris comme dans un terminal Unix

Selectionner copie aussitot au relachement du bouton, et le clic droit colle
directement. Plus de menu contextuel ni de raccourci a connaitre : c'est le
comportement des terminaux Unix et de PuTTY.

- selection relachee, et double-clic sur un mot, copient dans la foulee
- clic droit et clic molette collent immediatement
- un simple clic sans deplacement se contente d'annuler la selection, sans
  toucher au presse-papiers : sinon il ecraserait ce qu'on s'apprete a coller
- le menu contextuel est supprime, le clic droit etant desormais un collage
- "Tout selectionner" passe sur Ctrl+Maj+A, Ctrl+A restant "debut de ligne"
  pour le shell

Les raccourcis clavier restent disponibles pour qui les prefere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
snoopy
2026-09-04 16:30:17 +02:00
parent fbf7f580bc
commit f641357073
3 changed files with 58 additions and 25 deletions

View File

@@ -5,9 +5,10 @@ Client SSH/SFTP personnel (type MobaXterm/Termius/Bitvise), 100% local, sans com
## Fonctionnalites ## Fonctionnalites
- Sessions SSH multiples dans des onglets, avec terminal ANSI colore et trois themes - Sessions SSH multiples dans des onglets, avec terminal ANSI colore et trois themes
- Copier-coller dans le terminal : selection a la souris, double-clic pour selectionner un mot, - Copier-coller **a la souris, comme dans un terminal Unix** : selectionner copie aussitot
`Ctrl+Shift+C` / `Ctrl+Shift+V`, `Maj+Inser`, clic molette, et menu au clic droit. (double-clic pour un mot), et le clic droit colle directement — ni menu, ni raccourci.
`Ctrl+C` reste l'interruption, comme dans tout terminal Le clic molette colle aussi. `Ctrl+Shift+C` / `Ctrl+Shift+V` / `Maj+Inser` restent
disponibles au clavier, et `Ctrl+C` reste l'interruption comme dans tout terminal
- Confort d'affichage : zoom `Ctrl+molette` ou `Ctrl+ +/-` (`Ctrl+0` remet a zero, la taille est - Confort d'affichage : zoom `Ctrl+molette` ou `Ctrl+ +/-` (`Ctrl+0` remet a zero, la taille est
memorisee), barre de defilement de l'historique, `Maj+PagePrec/Suiv`, `Ctrl+Fin` pour revenir memorisee), barre de defilement de l'historique, `Maj+PagePrec/Suiv`, `Ctrl+Fin` pour revenir
en direct, et recherche dans l'historique avec `Ctrl+F` en direct, et recherche dans l'historique avec `Ctrl+F`

View File

@@ -11,9 +11,9 @@ from __future__ import annotations
import pyte import pyte
from PySide6.QtCore import QRect, Qt, Signal from PySide6.QtCore import QRect, Qt, Signal
from PySide6.QtGui import ( from PySide6.QtGui import (
QColor, QFont, QFontMetrics, QPainter, QKeyEvent, QWheelEvent, QKeySequence, QColor, QFont, QFontMetrics, QPainter, QKeyEvent, QWheelEvent,
) )
from PySide6.QtWidgets import QWidget, QMenu, QApplication, QScrollBar from PySide6.QtWidgets import QWidget, QApplication, QScrollBar
from app import theme as app_theme from app import theme as app_theme
@@ -478,6 +478,10 @@ class TerminalWidget(QWidget):
if key == Qt.Key_V: if key == Qt.Key_V:
self.paste_clipboard() self.paste_clipboard()
return return
if key == Qt.Key_A:
# Ctrl+A seul reste "debut de ligne" pour le shell
self.select_all()
return
if key == Qt.Key_Insert and (modifiers & Qt.ShiftModifier): if key == Qt.Key_Insert and (modifiers & Qt.ShiftModifier):
self.paste_clipboard() self.paste_clipboard()
return return
@@ -551,8 +555,10 @@ class TerminalWidget(QWidget):
self._sel_anchor = self._sel_focus = self._cell_at(event.position().toPoint()) self._sel_anchor = self._sel_focus = self._cell_at(event.position().toPoint())
self._selecting = True self._selecting = True
self.update() self.update()
elif event.button() == Qt.MiddleButton: elif event.button() in (Qt.RightButton, Qt.MiddleButton):
self.paste_clipboard() # habitude Unix : clic molette = coller # Comportement des terminaux Unix (et de PuTTY) : le clic droit colle
# directement, sans passer par un menu.
self.paste_clipboard()
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseMoveEvent(self, event) -> None: def mouseMoveEvent(self, event) -> None:
@@ -564,9 +570,14 @@ class TerminalWidget(QWidget):
def mouseReleaseEvent(self, event) -> None: def mouseReleaseEvent(self, event) -> None:
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self._selecting = False self._selecting = False
# un simple clic (sans deplacement) annule la selection
if self._sel_anchor == self._sel_focus: if self._sel_anchor == self._sel_focus:
# simple clic sans deplacement : on annule la selection sans toucher
# au presse-papiers, qui contient peut-etre ce qu'on s'apprete a coller
self.clear_selection() self.clear_selection()
else:
# Comportement des terminaux Unix : relacher la souris apres une
# selection copie immediatement, sans raccourci ni menu.
self.copy_selection()
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
def mouseDoubleClickEvent(self, event) -> None: def mouseDoubleClickEvent(self, event) -> None:
@@ -584,22 +595,9 @@ class TerminalWidget(QWidget):
end += 1 end += 1
self._sel_anchor, self._sel_focus = (start, row), (end, row) self._sel_anchor, self._sel_focus = (start, row), (end, row)
self.update() self.update()
self.copy_selection() # comme la selection a la souris : copie immediate
def contextMenuEvent(self, event) -> None: def contextMenuEvent(self, event) -> None:
menu = QMenu(self) # Pas de menu contextuel : dans un terminal Unix le clic droit colle,
copy_action = menu.addAction("Copier") # et il est traite dans mousePressEvent.
copy_action.setShortcut(QKeySequence("Ctrl+Shift+C")) event.ignore()
copy_action.setEnabled(self.has_selection())
paste_action = menu.addAction("Coller")
paste_action.setShortcut(QKeySequence("Ctrl+Shift+V"))
paste_action.setEnabled(bool(QApplication.clipboard().text()))
menu.addSeparator()
select_all_action = menu.addAction("Tout selectionner")
action = menu.exec(event.globalPos())
if action == copy_action:
self.copy_selection()
elif action == paste_action:
self.paste_clipboard()
elif action == select_all_action:
self.select_all()

View File

@@ -88,5 +88,39 @@ t.clear_selection()
assert not t.has_selection() assert not t.has_selection()
print(" OK") print(" OK")
print()
print("=== 9. Souris : selection = copie, clic droit = collage (comme sous Unix) ===")
from PySide6.QtGui import QMouseEvent, QContextMenuEvent
from PySide6.QtCore import QPointF
def clic(bouton, col, row, type_evt=QEvent.MouseButtonPress):
pos = QPointF(col * t.char_w + 1, row * t.char_h + 1)
return QMouseEvent(type_evt, pos, pos, bouton, bouton, Qt.NoModifier)
QApplication.clipboard().setText("")
t.mousePressEvent(clic(Qt.LeftButton, 0, 1))
t._sel_focus = (14, 1)
t.mouseReleaseEvent(clic(Qt.LeftButton, 14, 1, QEvent.MouseButtonRelease))
print(" selection relachee -> presse-papiers :", repr(QApplication.clipboard().text()))
assert QApplication.clipboard().text() == "serveur-test-01", "la selection doit copier sans raccourci"
envoye.clear()
t.mousePressEvent(clic(Qt.RightButton, 5, 0))
print(" clic droit -> envoye au shell :", envoye)
assert envoye == [b"serveur-test-01"], "le clic droit doit coller directement"
QApplication.clipboard().setText("a-conserver")
t.mousePressEvent(clic(Qt.LeftButton, 3, 0))
t.mouseReleaseEvent(clic(Qt.LeftButton, 3, 0, QEvent.MouseButtonRelease))
assert QApplication.clipboard().text() == "a-conserver", "un simple clic ne doit rien ecraser"
print(" simple clic -> presse-papiers intact")
ev = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10), QPoint(10, 10))
t.contextMenuEvent(ev)
assert not ev.isAccepted(), "aucun menu contextuel ne doit s'ouvrir"
print(" clic droit -> aucun menu contextuel")
print() print()
print("COPIER/COLLER : TOUS LES TESTS OK") print("COPIER/COLLER : TOUS LES TESTS OK")