Skip to content
Open
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
7 changes: 7 additions & 0 deletions GlobalStates.qml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Singleton {
property bool settingsOverlayOpen: false
property int settingsOverlayRequestedPage: -1 // Set before opening to navigate to a specific page
property bool regionSelectorOpen: false
property bool screenshotEditorOpen: false // Unified capture+crop+annotate flow (screenshotEdit action)
// Which output owns the screenshot editor session (first click claims; others stay dim-only).
property string screenshotEditorActiveScreen: ""
onScreenshotEditorOpenChanged: {
if (!screenshotEditorOpen)
screenshotEditorActiveScreen = ""
}
// Native screenshot annotation editor (Edit action)
property bool annotationEditorOpen: false
property string annotationEditorPath: ""
Expand Down
5 changes: 4 additions & 1 deletion defaults/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,10 @@
"annotation": {
"useSatty": false,
"useNativeEditor": true
}
},
"customColors": [],
"lastCustomColor": "",
"lastStrokeWidth": 4
},
"resources": {
"updateInterval": 3000,
Expand Down
2 changes: 1 addition & 1 deletion defaults/niri/config.d/70-binds.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ binds {
Mod+Ctrl+Shift+L allow-when-locked=true { spawn "inir" "lock" "focus"; }

// Region selector: screenshot / OCR / Google Lens search.
Mod+Shift+S { spawn "inir" "region" "screenshot"; }
Mod+Shift+S { spawn "inir" "region" "screenshotEdit"; }
Mod+Shift+X { spawn "inir" "region" "ocr"; }
Mod+Shift+A { spawn "inir" "region" "search"; }
// Unified snip menu (pick action/scope from the in-overlay toolbar).
Expand Down
5 changes: 4 additions & 1 deletion docs/IPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,19 @@ Region selection tools. Screenshots, OCR, recording. Draw a box, get stuff done.
| Function | Description |
|----------|-------------|
| `screenshot` | Take a region screenshot |
| `screenshotEdit` | Select a region and open it in the annotation editor |
| `search` | Image search (Google Lens) |
| `googleLens` | Start a region capture for Google Lens |
| `ocr` | OCR text recognition |
| `record` | Record region (no audio) |
| `recordWithSound` | Record region with audio |
| `menu` | Open the unified snip menu (pick action/scope inline) |

```kdl
bind "Super+Shift+S" { spawn "inir" "region" "screenshot"; }
bind "Super+Shift+S" { spawn "inir" "region" "screenshotEdit"; }
bind "Super+Shift+X" { spawn "inir" "region" "ocr"; }
bind "Super+Shift+A" { spawn "inir" "region" "search"; }
bind "Ctrl+Shift+S" { spawn "inir" "region" "menu"; }
```

---
Expand Down
3 changes: 2 additions & 1 deletion docs/KEYBINDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ Change them. Break them. Make them yours. We won't judge.

| Key | Action |
|-----|--------|
| `Mod+Shift+S` | Region screenshot |
| `Mod+Shift+S` | Region screenshot & annotate |
| `Mod+Shift+X` | Region OCR |
| `Mod+Shift+A` | Region image search |
| `Ctrl+Shift+S` | Unified snip menu |
| `Print` | Full screenshot (Niri native) |
| `Ctrl+Print` | Screenshot current screen |
| `Alt+Print` | Screenshot current window |
Expand Down
2 changes: 1 addition & 1 deletion dots/.config/niri/config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ binds {
Mod+Alt+L allow-when-locked=true { spawn "qs" "-c" "inir" "ipc" "call" "lock" "activate"; }

// ii Region tools
Mod+Shift+S { spawn "qs" "-c" "inir" "ipc" "call" "region" "screenshot"; }
Mod+Shift+S { spawn "qs" "-c" "inir" "ipc" "call" "region" "screenshotEdit"; }
Mod+Shift+X { spawn "qs" "-c" "inir" "ipc" "call" "region" "ocr"; }
Mod+Shift+A { spawn "qs" "-c" "inir" "ipc" "call" "region" "search"; }

Expand Down
3 changes: 3 additions & 0 deletions modules/common/Config.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,9 @@ Singleton {
property bool useNativeEditor: true
}
property string screenshotNameFormat: "ss-%Y%m%d-%H%M%S" // date(1) format for screenshot filenames (without extension)
property list<string> customColors: [] // Annotation editor's "My Collection" saved custom colors
property string lastCustomColor: "" // Last custom color picked, shown on the picker swatch itself
property real lastStrokeWidth: 4 // Last annotation stroke width used
}

property JsonObject resources: JsonObject {
Expand Down
8 changes: 7 additions & 1 deletion modules/dock/Dock.qml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ Scope {
sourceComponent: PanelWindow {
id: dockRoot
screen: panelLoader.modelData
visible: !GlobalStates.screenLocked && !GameMode.shouldHidePanels && !GlobalStates.widgetEditMode

// Anything in here hides the dock outright.
readonly property bool hiddenByOverlay: GlobalStates.screenLocked
|| GameMode.shouldHidePanels
|| GlobalStates.widgetEditMode
|| GlobalStates.screenshotEditorOpen
visible: !hiddenByOverlay

property bool reveal: !GlobalStates.coverflowSelectorOpen && GlobalStates.shellEntryReady && (root.pinned || (Config.options?.dock?.hoverToReveal && dockMouseArea.containsMouse) || (dockApps?.requestDockShow || dockAppsVertical?.requestDockShow) || (Config.options?.dock?.showOnDesktop !== false && !ToplevelManager.activeToplevel?.activated))

Expand Down
283 changes: 283 additions & 0 deletions modules/regionSelector/AnnotationColorPicker.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

// Custom-color swatch + in-window HSV picker popup with a saved-colors
// collection. Shared by the ii (Material) and waffle (Fluent) toolbars —
// all state/behavior comes from the injected `editor` (the ScreenshotEditor).
Rectangle {
id: customSwatch
required property var editor

implicitWidth: 26
implicitHeight: 26
radius: Appearance.rounding.full
border.width: editor.strokeColorIsCustom ? 3 : 1
border.color: editor.strokeColorIsCustom
? Appearance.colors.colOnLayer1
: Appearance.colors.colOutlineVariant
Behavior on border.width {
enabled: Appearance.animationsEnabled
animation: NumberAnimation { duration: Appearance.animation.elementMoveFast.duration; easing.type: Appearance.animation.elementMoveFast.type }
}
// Shows the last custom color once you've picked one; rainbow until then.
color: editor.lastCustomColor !== "" ? editor.lastCustomColor : "transparent"
gradient: editor.lastCustomColor === "" ? rainbowGradient : null
Gradient {
id: rainbowGradient
GradientStop { position: 0.0; color: "#ff5252" }
GradientStop { position: 0.33; color: "#4caf50" }
GradientStop { position: 0.66; color: "#2196f3" }
GradientStop { position: 1.0; color: "#e040fb" }
}
MaterialSymbol {
anchors.centerIn: parent
visible: customSwatch.editor.lastCustomColor === ""
text: "colorize"
iconSize: 14
color: "#ffffff"
}

property real pickHue: 0
property real pickSat: 0
property real pickValue: 1
property string hexText: "#000000"
function syncFromStrokeColor() {
pickHue = Math.max(0, editor.strokeColor.hsvHue);
pickSat = editor.strokeColor.hsvSaturation;
pickValue = editor.strokeColor.hsvValue;
hexText = editor.strokeColor.toString();
}
function applyHsv() {
editor.setStrokeColor(Qt.hsva(pickHue, pickSat, pickValue, 1));
hexText = editor.strokeColor.toString();
editor.setLastCustomColor(hexText);
}

property bool hovered: swatchMouseArea.containsMouse
MouseArea {
id: swatchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: { customSwatch.syncFromStrokeColor(); colorPickerPopup.open(); }
}
StyledToolTip { text: Translation.tr("Custom color") }

Popup {
id: colorPickerPopup
y: -height - 10
x: (customSwatch.width - width) / 2
width: 232
padding: 14
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: customSwatch.editor.suppressGlobalKeys = true
onClosed: {
customSwatch.editor.suppressGlobalKeys = false
customSwatch.editor.focusKeySink()
}

background: GlassBackground {
fallbackColor: Appearance.m3colors.m3surfaceContainer
inirColor: Appearance.inir.colLayer2
auroraTransparency: Appearance.aurora.overlayTransparentize
screenWidth: customSwatch.editor.width
screenHeight: customSwatch.editor.height
radius: Appearance.rounding.normal
border.width: (Appearance.angelEverywhere || Appearance.inirEverywhere || Appearance.auroraEverywhere) ? 1 : 0
border.color: Appearance.angelEverywhere ? Appearance.angel.colBorder
: Appearance.inirEverywhere ? Appearance.inir.colBorder
: Appearance.auroraEverywhere ? Appearance.aurora.colTooltipBorder : "transparent"
}

contentItem: ColumnLayout {
spacing: 12

Item {
id: svSquare
Layout.fillWidth: true
Layout.preferredHeight: 140

function updateFromPos(px, py) {
const cx = Math.max(0, Math.min(width, px));
const cy = Math.max(0, Math.min(height, py));
customSwatch.pickSat = cx / width;
customSwatch.pickValue = 1 - cy / height;
customSwatch.applyHsv();
}

Rectangle {
anchors.fill: parent
radius: Appearance.rounding.small
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#ffffff" }
GradientStop { position: 1.0; color: Qt.hsva(customSwatch.pickHue, 1, 1, 1) }
}
}
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.small
gradient: Gradient {
GradientStop { position: 0.0; color: "#00000000" }
GradientStop { position: 1.0; color: "#ff000000" }
}
}
Rectangle {
x: customSwatch.pickSat * svSquare.width - width / 2
y: (1 - customSwatch.pickValue) * svSquare.height - height / 2
width: 14
height: 14
radius: 7
color: "transparent"
border.width: 2
border.color: "#ffffff"
}
MouseArea {
anchors.fill: parent
onPressed: (m) => svSquare.updateFromPos(m.x, m.y)
onPositionChanged: (m) => { if (pressed) svSquare.updateFromPos(m.x, m.y) }
}
}

Item {
id: hueTrack
Layout.fillWidth: true
Layout.preferredHeight: 18

function updateFromPos(px) {
customSwatch.pickHue = Math.max(0, Math.min(1, px / width));
customSwatch.applyHsv();
}

Rectangle {
anchors.fill: parent
radius: height / 2
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#ff0000" }
GradientStop { position: 0.166; color: "#ffff00" }
GradientStop { position: 0.333; color: "#00ff00" }
GradientStop { position: 0.5; color: "#00ffff" }
GradientStop { position: 0.666; color: "#0000ff" }
GradientStop { position: 0.833; color: "#ff00ff" }
GradientStop { position: 1.0; color: "#ff0000" }
}
}
Rectangle {
x: customSwatch.pickHue * (hueTrack.width - width)
y: -2
width: 6
height: hueTrack.height + 4
radius: 3
color: "#ffffff"
border.width: 1
border.color: "#55000000"
}
MouseArea {
anchors.fill: parent
onPressed: (m) => hueTrack.updateFromPos(m.x)
onPositionChanged: (m) => { if (pressed) hueTrack.updateFromPos(m.x) }
}
}

ToolbarTextField {
Layout.fillWidth: true
implicitHeight: 44
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.normal
font.family: Appearance.font.family.monospace
horizontalAlignment: Text.AlignHCenter
text: customSwatch.hexText
onEditingFinished: {
customSwatch.editor.setStrokeColor(text);
customSwatch.syncFromStrokeColor();
customSwatch.editor.setLastCustomColor(customSwatch.hexText);
}
}

StyledText {
text: Translation.tr("My Collection")
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
}

Flow {
Layout.fillWidth: true
spacing: 6

Repeater {
model: customSwatch.editor.palette
delegate: Rectangle {
id: baseSwatch
required property var modelData
implicitWidth: 22
implicitHeight: 22
radius: Appearance.rounding.full
color: baseSwatch.modelData
border.width: customSwatch.editor.strokeColor == baseSwatch.modelData ? 3 : 1
border.color: customSwatch.editor.strokeColor == baseSwatch.modelData
? Appearance.colors.colOnLayer1
: Appearance.colors.colOutlineVariant
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: customSwatch.editor.setStrokeColor(baseSwatch.modelData)
}
}
}

Repeater {
model: customSwatch.editor.savedCustomColors
delegate: Rectangle {
id: savedSwatch
required property var modelData
implicitWidth: 22
implicitHeight: 22
radius: Appearance.rounding.full
color: savedSwatch.modelData
border.width: customSwatch.editor.strokeColor == savedSwatch.modelData ? 3 : 1
border.color: customSwatch.editor.strokeColor == savedSwatch.modelData
? Appearance.colors.colOnLayer1
: Appearance.colors.colOutlineVariant
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
customSwatch.editor.setStrokeColor(savedSwatch.modelData);
customSwatch.syncFromStrokeColor();
customSwatch.editor.setLastCustomColor(savedSwatch.modelData);
}
}
}
}

Rectangle {
implicitWidth: 22
implicitHeight: 22
radius: Appearance.rounding.full
color: "transparent"
border.width: 1
border.color: Appearance.colors.colOutlineVariant
MaterialSymbol {
anchors.centerIn: parent
text: "add"
iconSize: 14
color: Appearance.colors.colSubtext
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: customSwatch.editor.saveCustomColor(customSwatch.hexText)
}
}
}
}
}
}
Loading