409 lines
15 KiB
QML
409 lines
15 KiB
QML
// Focused result sheet for local OCR and code recognition. It is intentionally
|
||
// a transient overlay: the user selects pixels, acts on the result, and leaves
|
||
// without creating another application window or workspace tile.
|
||
|
||
import Quickshell
|
||
import Quickshell.Wayland
|
||
import QtQuick
|
||
import qs.config
|
||
import qs.services
|
||
import qs.widgets
|
||
|
||
PanelWindow {
|
||
id: win
|
||
|
||
visible: ScreenIntelligence.visible
|
||
color: "transparent"
|
||
exclusiveZone: 0
|
||
|
||
anchors {
|
||
top: true
|
||
bottom: true
|
||
left: true
|
||
right: true
|
||
}
|
||
|
||
WlrLayershell.namespace: "qs-screen-intelligence"
|
||
WlrLayershell.layer: WlrLayer.Overlay
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||
|
||
onVisibleChanged: {
|
||
if (visible)
|
||
keyboardScope.forceActiveFocus();
|
||
else
|
||
copied = false;
|
||
}
|
||
|
||
property bool copied: false
|
||
|
||
Timer {
|
||
id: copiedTimer
|
||
interval: 1500
|
||
onTriggered: win.copied = false
|
||
}
|
||
|
||
FocusScope {
|
||
id: keyboardScope
|
||
anchors.fill: parent
|
||
focus: true
|
||
|
||
Keys.onEscapePressed: event => {
|
||
ScreenIntelligence.close();
|
||
event.accepted = true;
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: Theme.alpha(Theme.bgDark, 0.46)
|
||
border.width: 0
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: ScreenIntelligence.close()
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
id: sheet
|
||
anchors.centerIn: parent
|
||
width: Math.min(parent.width - 80, 840)
|
||
height: Math.min(parent.height - 120, ScreenIntelligence.phase === "ready" ? 680 : 390)
|
||
radius: 24
|
||
color: Theme.alpha(Theme.bgPopover, 0.965)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.fg, 0.1)
|
||
clip: true
|
||
|
||
PrismEdge {
|
||
anchors.top: parent.top
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
inset: sheet.radius
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: mouse => mouse.accepted = true
|
||
}
|
||
|
||
Column {
|
||
id: heading
|
||
anchors.left: parent.left
|
||
anchors.right: closeButton.left
|
||
anchors.top: parent.top
|
||
anchors.leftMargin: 28
|
||
anchors.topMargin: 25
|
||
anchors.rightMargin: 18
|
||
spacing: 4
|
||
|
||
Text {
|
||
text: "Screen Intelligence"
|
||
color: Theme.fg
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeTitle
|
||
font.weight: Font.DemiBold
|
||
}
|
||
|
||
Text {
|
||
text: {
|
||
if (ScreenIntelligence.phase === "capturing")
|
||
return "Capturing the selected pixels";
|
||
if (ScreenIntelligence.phase === "reading")
|
||
return "Reading locally on this computer";
|
||
if (ScreenIntelligence.phase === "error")
|
||
return "Recognition needs attention";
|
||
return "Local text and code recognition";
|
||
}
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSize
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
id: closeButton
|
||
anchors.top: parent.top
|
||
anchors.right: parent.right
|
||
anchors.topMargin: 20
|
||
anchors.rightMargin: 20
|
||
width: 34
|
||
height: 34
|
||
radius: 11
|
||
color: closeMouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha) : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "×"
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: 20
|
||
}
|
||
|
||
MouseArea {
|
||
id: closeMouse
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: ScreenIntelligence.close()
|
||
}
|
||
}
|
||
|
||
Item {
|
||
id: body
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: heading.bottom
|
||
anchors.bottom: actions.top
|
||
anchors.leftMargin: 28
|
||
anchors.rightMargin: 28
|
||
anchors.topMargin: 24
|
||
anchors.bottomMargin: 18
|
||
|
||
Column {
|
||
anchors.centerIn: parent
|
||
width: Math.min(parent.width, 520)
|
||
spacing: 13
|
||
visible: ScreenIntelligence.busy
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: ""
|
||
color: Theme.accent
|
||
font.family: Theme.fontMono
|
||
font.pixelSize: 40
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: ScreenIntelligence.phase === "capturing" ? "Capturing…" : "Reading text and codes…"
|
||
color: Theme.fg
|
||
horizontalAlignment: Text.AlignHCenter
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
font.weight: Font.DemiBold
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: "Pixels stay on this computer. No network request is being made."
|
||
color: Theme.fgDim
|
||
horizontalAlignment: Text.AlignHCenter
|
||
wrapMode: Text.WordWrap
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSize
|
||
}
|
||
}
|
||
|
||
Column {
|
||
anchors.centerIn: parent
|
||
width: Math.min(parent.width, 560)
|
||
spacing: 14
|
||
visible: ScreenIntelligence.phase === "error"
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: ""
|
||
color: Theme.warn
|
||
font.family: Theme.fontMono
|
||
font.pixelSize: 38
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: ScreenIntelligence.error
|
||
color: Theme.fg
|
||
horizontalAlignment: Text.AlignHCenter
|
||
wrapMode: Text.WordWrap
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
font.weight: Font.DemiBold
|
||
}
|
||
Rectangle {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
width: commandText.implicitWidth + 28
|
||
height: 38
|
||
radius: 10
|
||
visible: !ScreenIntelligence.ocrReady
|
||
color: Theme.alpha(Theme.fg, 0.07)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.fg, 0.08)
|
||
|
||
Text {
|
||
id: commandText
|
||
anchors.centerIn: parent
|
||
text: "sudo dnf install -y tesseract zbar"
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontMono
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
}
|
||
}
|
||
}
|
||
|
||
Column {
|
||
anchors.fill: parent
|
||
spacing: 12
|
||
visible: ScreenIntelligence.phase === "ready"
|
||
|
||
Rectangle {
|
||
width: parent.width
|
||
height: ScreenIntelligence.codeValue === "" ? 0 : 62
|
||
visible: ScreenIntelligence.codeValue !== ""
|
||
radius: 13
|
||
color: Theme.alpha(Theme.accent, 0.09)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.accent, 0.2)
|
||
|
||
Text {
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: 16
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: ""
|
||
color: Theme.accent
|
||
font.family: Theme.fontMono
|
||
font.pixelSize: 22
|
||
}
|
||
Column {
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: 52
|
||
anchors.right: codeAction.left
|
||
anchors.rightMargin: 14
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 2
|
||
Text { text: ScreenIntelligence.codeType || "Detected code"; color: Theme.fg; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSizeSmall; font.weight: Font.DemiBold }
|
||
Text { width: parent.width; text: ScreenIntelligence.codeValue; color: Theme.fgDim; elide: Text.ElideMiddle; font.family: Theme.fontFamily; font.pixelSize: Theme.fontSize }
|
||
}
|
||
ResultAction {
|
||
id: codeAction
|
||
anchors.right: parent.right
|
||
anchors.rightMargin: 12
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
label: ScreenIntelligence.primaryUrl !== "" ? "Open" : "Copy"
|
||
onClicked: {
|
||
if (ScreenIntelligence.primaryUrl !== "")
|
||
ScreenIntelligence.openDetected();
|
||
else
|
||
ScreenIntelligence.copyCode();
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: parent.width
|
||
height: parent.height - (ScreenIntelligence.codeValue === "" ? 0 : 74)
|
||
radius: 14
|
||
color: Theme.alpha(Theme.bgDark, 0.72)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.fg, 0.075)
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
width: parent.width - 60
|
||
visible: ScreenIntelligence.text === "" && ScreenIntelligence.codeValue === ""
|
||
text: "No readable text or code was found. Try a tighter selection with larger, higher-contrast text."
|
||
color: Theme.fgDim
|
||
horizontalAlignment: Text.AlignHCenter
|
||
wrapMode: Text.WordWrap
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
}
|
||
|
||
Flickable {
|
||
anchors.fill: parent
|
||
anchors.margins: 17
|
||
visible: ScreenIntelligence.text !== ""
|
||
clip: true
|
||
contentWidth: width
|
||
contentHeight: resultText.implicitHeight
|
||
boundsBehavior: Flickable.StopAtBounds
|
||
|
||
TextEdit {
|
||
id: resultText
|
||
width: parent.width
|
||
text: ScreenIntelligence.text
|
||
readOnly: true
|
||
selectByMouse: true
|
||
wrapMode: TextEdit.Wrap
|
||
color: Theme.fg
|
||
selectionColor: Theme.accent
|
||
selectedTextColor: Theme.bgDark
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Row {
|
||
id: actions
|
||
anchors.right: parent.right
|
||
anchors.bottom: parent.bottom
|
||
anchors.rightMargin: 28
|
||
anchors.bottomMargin: 24
|
||
spacing: 8
|
||
visible: ScreenIntelligence.phase === "ready" && (ScreenIntelligence.text !== "" || ScreenIntelligence.codeValue !== "")
|
||
|
||
ResultAction {
|
||
label: "Open link"
|
||
visible: ScreenIntelligence.primaryUrl !== "" && ScreenIntelligence.codeValue === ""
|
||
onClicked: ScreenIntelligence.openDetected()
|
||
}
|
||
ResultAction {
|
||
label: "Translate"
|
||
enabled: ScreenIntelligence.text !== ""
|
||
onClicked: ScreenIntelligence.translate()
|
||
}
|
||
ResultAction {
|
||
label: "Search"
|
||
enabled: ScreenIntelligence.text !== ""
|
||
onClicked: ScreenIntelligence.search()
|
||
}
|
||
ResultAction {
|
||
label: win.copied ? "Copied" : "Copy text"
|
||
primary: true
|
||
enabled: ScreenIntelligence.text !== ""
|
||
onClicked: {
|
||
ScreenIntelligence.copyText();
|
||
win.copied = true;
|
||
copiedTimer.restart();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
component ResultAction: Rectangle {
|
||
id: action
|
||
property string label: ""
|
||
property bool primary: false
|
||
property bool enabled: true
|
||
signal clicked
|
||
|
||
implicitWidth: actionLabel.implicitWidth + 26
|
||
implicitHeight: 34
|
||
radius: 10
|
||
opacity: enabled ? 1 : 0.4
|
||
color: primary
|
||
? (actionMouse.containsMouse ? Theme.mix(Theme.accent, Theme.fg, 0.1) : Theme.accent)
|
||
: (actionMouse.containsMouse ? Theme.alpha(Theme.fg, 0.13) : Theme.alpha(Theme.fg, 0.075))
|
||
border.width: primary ? 0 : 1
|
||
border.color: Theme.alpha(Theme.fg, 0.08)
|
||
|
||
Text {
|
||
id: actionLabel
|
||
anchors.centerIn: parent
|
||
text: action.label
|
||
color: action.primary ? Theme.bgDark : Theme.fg
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.weight: Font.Medium
|
||
}
|
||
|
||
MouseArea {
|
||
id: actionMouse
|
||
anchors.fill: parent
|
||
enabled: action.enabled
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: action.clicked()
|
||
}
|
||
}
|
||
}
|