Skip to content

Commit fff60b9

Browse files
luginfluginf
andauthored
Snippets (#287)
* first commit * linter * lint again * add missing file * fix various copilot suggestions --------- Co-authored-by: luginf <alan@luginf>
1 parent 2ef1198 commit fff60b9

6 files changed

Lines changed: 1168 additions & 0 deletions

File tree

snippets/InsertSnippetDialog.qml

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
import QtQuick 2.0
2+
import QtQuick.Window 2.0
3+
4+
Window {
5+
id: root
6+
title: "Insert Snippet"
7+
width: 580
8+
height: 200
9+
minimumWidth: 400
10+
minimumHeight: 160
11+
modality: Qt.ApplicationModal
12+
flags: Qt.Dialog | Qt.WindowCloseButtonHint
13+
14+
// entries: [{name, preview, originalIndex}]
15+
property var entries: []
16+
signal snippetChosen(int index)
17+
signal manageRequested
18+
property var filtered: []
19+
20+
SystemPalette {
21+
id: pal
22+
}
23+
24+
Component.onCompleted: {
25+
applyFilter();
26+
searchInput.forceActiveFocus();
27+
}
28+
29+
// ── Search field ──────────────────────────────────────────────────────────
30+
Rectangle {
31+
id: searchBox
32+
anchors {
33+
top: parent.top
34+
left: parent.left
35+
right: parent.right
36+
margins: 8
37+
}
38+
height: 26
39+
radius: 3
40+
color: pal.base
41+
border.color: searchInput.activeFocus ? "#1cb27e" : pal.mid
42+
border.width: 1
43+
44+
Text {
45+
anchors {
46+
fill: parent
47+
leftMargin: 7
48+
}
49+
verticalAlignment: Text.AlignVCenter
50+
text: "Filter…"
51+
color: pal.mid
52+
font.pixelSize: 12
53+
visible: searchInput.text === ""
54+
}
55+
56+
TextInput {
57+
id: searchInput
58+
anchors {
59+
fill: parent
60+
margins: 7
61+
}
62+
verticalAlignment: TextInput.AlignVCenter
63+
font.pixelSize: 12
64+
color: pal.text
65+
clip: true
66+
onTextChanged: applyFilter()
67+
Keys.onReturnPressed: acceptSelection()
68+
Keys.onDownPressed: moveSelection(1)
69+
Keys.onUpPressed: moveSelection(-1)
70+
}
71+
}
72+
73+
// ── Bottom bar ────────────────────────────────────────────────────────────
74+
Item {
75+
id: bottomBar
76+
anchors {
77+
bottom: parent.bottom
78+
left: parent.left
79+
right: parent.right
80+
margins: 8
81+
}
82+
height: 30
83+
84+
// Manage button (left side)
85+
Rectangle {
86+
id: manageBtn
87+
anchors {
88+
verticalCenter: parent.verticalCenter
89+
left: parent.left
90+
}
91+
width: 70
92+
height: 24
93+
radius: 4
94+
color: manageMouse.pressed ? pal.dark : pal.button
95+
border.color: pal.mid
96+
border.width: 1
97+
98+
Text {
99+
anchors.centerIn: parent
100+
text: "Manage…"
101+
color: pal.buttonText
102+
font.pixelSize: 12
103+
}
104+
105+
MouseArea {
106+
id: manageMouse
107+
anchors.fill: parent
108+
onClicked: {
109+
manageRequested();
110+
root.close();
111+
}
112+
}
113+
}
114+
115+
Rectangle {
116+
id: cancelBtn
117+
anchors {
118+
verticalCenter: parent.verticalCenter
119+
right: insertBtn.left
120+
rightMargin: 6
121+
}
122+
width: 64
123+
height: 24
124+
radius: 4
125+
color: cancelMouse.pressed ? pal.dark : pal.button
126+
border.color: pal.mid
127+
border.width: 1
128+
129+
Text {
130+
anchors.centerIn: parent
131+
text: "Cancel"
132+
color: pal.buttonText
133+
font.pixelSize: 12
134+
}
135+
136+
MouseArea {
137+
id: cancelMouse
138+
anchors.fill: parent
139+
onClicked: root.close()
140+
}
141+
}
142+
143+
Rectangle {
144+
id: insertBtn
145+
anchors {
146+
verticalCenter: parent.verticalCenter
147+
right: parent.right
148+
}
149+
width: 64
150+
height: 24
151+
radius: 4
152+
opacity: snippetList.currentIndex >= 0 && filtered.length > 0 ? 1.0 : 0.4
153+
color: insertMouse.pressed ? "#15896b" : "#1cb27e"
154+
155+
Text {
156+
anchors.centerIn: parent
157+
text: "Insert"
158+
color: "white"
159+
font.pixelSize: 12
160+
}
161+
162+
MouseArea {
163+
id: insertMouse
164+
anchors.fill: parent
165+
enabled: snippetList.currentIndex >= 0 && filtered.length > 0
166+
onClicked: acceptSelection()
167+
}
168+
}
169+
}
170+
171+
// ── Left panel: snippet list ──────────────────────────────────────────────
172+
Rectangle {
173+
id: listPanel
174+
anchors {
175+
top: searchBox.bottom
176+
topMargin: 5
177+
left: parent.left
178+
leftMargin: 8
179+
bottom: bottomBar.top
180+
bottomMargin: 5
181+
}
182+
width: 180
183+
radius: 3
184+
color: pal.base
185+
border.color: pal.mid
186+
border.width: 1
187+
clip: true
188+
189+
ListView {
190+
id: snippetList
191+
anchors {
192+
fill: parent
193+
margins: 1
194+
rightMargin: listScroll.visible ? 8 : 1
195+
}
196+
model: filtered
197+
currentIndex: filtered.length > 0 ? 0 : -1
198+
clip: true
199+
boundsBehavior: Flickable.StopAtBounds
200+
201+
delegate: Item {
202+
width: snippetList.width
203+
height: 24
204+
205+
Rectangle {
206+
anchors.fill: parent
207+
color: index === snippetList.currentIndex ? "#1cb27e" : (rowMouse.containsMouse ? "#e4f5ef" : "transparent")
208+
}
209+
210+
Text {
211+
anchors {
212+
verticalCenter: parent.verticalCenter
213+
left: parent.left
214+
right: parent.right
215+
margins: 7
216+
}
217+
text: modelData.name
218+
color: index === snippetList.currentIndex ? "white" : pal.text
219+
font.pixelSize: 12
220+
elide: Text.ElideRight
221+
}
222+
223+
MouseArea {
224+
id: rowMouse
225+
anchors.fill: parent
226+
hoverEnabled: true
227+
onClicked: snippetList.currentIndex = index
228+
onDoubleClicked: acceptSelection()
229+
}
230+
}
231+
232+
onCurrentIndexChanged: {
233+
if (currentIndex >= 0 && currentIndex < filtered.length)
234+
previewEdit.text = filtered[currentIndex].preview;
235+
else
236+
previewEdit.text = "";
237+
}
238+
}
239+
240+
Rectangle {
241+
id: listScroll
242+
visible: snippetList.contentHeight > snippetList.height
243+
width: 4
244+
anchors {
245+
right: parent.right
246+
top: parent.top
247+
bottom: parent.bottom
248+
margins: 1
249+
}
250+
color: "transparent"
251+
252+
Rectangle {
253+
width: parent.width
254+
radius: 2
255+
color: pal.mid
256+
height: Math.max(20, snippetList.height * snippetList.height / Math.max(snippetList.contentHeight, 1))
257+
y: snippetList.height > 0 ? snippetList.contentY / Math.max(snippetList.contentHeight - snippetList.height, 1) * (snippetList.height - height) : 0
258+
}
259+
}
260+
261+
Text {
262+
anchors.centerIn: parent
263+
visible: filtered.length === 0
264+
text: "No match."
265+
color: pal.mid
266+
font.pixelSize: 12
267+
}
268+
}
269+
270+
// ── Right panel: preview ──────────────────────────────────────────────────
271+
Text {
272+
id: previewLabel
273+
anchors {
274+
top: searchBox.bottom
275+
topMargin: 8
276+
left: listPanel.right
277+
leftMargin: 10
278+
}
279+
text: "Preview"
280+
color: pal.mid
281+
font.pixelSize: 10
282+
}
283+
284+
Rectangle {
285+
id: previewPanel
286+
anchors {
287+
top: previewLabel.bottom
288+
topMargin: 2
289+
left: listPanel.right
290+
leftMargin: 8
291+
right: parent.right
292+
rightMargin: 8
293+
bottom: bottomBar.top
294+
bottomMargin: 5
295+
}
296+
radius: 3
297+
color: pal.base
298+
border.color: pal.mid
299+
border.width: 1
300+
clip: true
301+
302+
Flickable {
303+
id: previewFlick
304+
anchors {
305+
fill: parent
306+
margins: 7
307+
rightMargin: previewScroll.visible ? 12 : 7
308+
}
309+
contentWidth: width
310+
contentHeight: previewEdit.implicitHeight
311+
flickableDirection: Flickable.VerticalFlick
312+
clip: true
313+
314+
TextEdit {
315+
id: previewEdit
316+
width: previewFlick.width
317+
height: Math.max(previewFlick.height, implicitHeight)
318+
readOnly: true
319+
selectByMouse: true
320+
wrapMode: Text.Wrap
321+
font.pixelSize: 12
322+
font.family: "monospace"
323+
color: pal.text
324+
}
325+
}
326+
327+
Rectangle {
328+
id: previewScroll
329+
visible: previewFlick.contentHeight > previewFlick.height
330+
width: 4
331+
anchors {
332+
right: parent.right
333+
top: parent.top
334+
bottom: parent.bottom
335+
margins: 1
336+
}
337+
color: "transparent"
338+
339+
Rectangle {
340+
width: parent.width
341+
radius: 2
342+
color: pal.mid
343+
height: Math.max(20, previewFlick.height * previewFlick.height / Math.max(previewFlick.contentHeight, 1))
344+
y: previewFlick.height > 0 ? previewFlick.contentY / Math.max(previewFlick.contentHeight - previewFlick.height, 1) * (previewFlick.height - height) : 0
345+
}
346+
}
347+
}
348+
349+
// ── Logic ─────────────────────────────────────────────────────────────────
350+
function moveSelection(delta) {
351+
var next = snippetList.currentIndex + delta;
352+
if (next >= 0 && next < snippetList.count)
353+
snippetList.currentIndex = next;
354+
}
355+
356+
function applyFilter() {
357+
var f = searchInput.text ? searchInput.text.toLowerCase() : "";
358+
var result = [];
359+
for (var i = 0; i < entries.length; i++) {
360+
if (!f || entries[i].name.toLowerCase().indexOf(f) >= 0)
361+
result.push(entries[i]);
362+
}
363+
filtered = result;
364+
snippetList.currentIndex = result.length > 0 ? 0 : -1;
365+
}
366+
367+
function acceptSelection() {
368+
var idx = snippetList.currentIndex;
369+
if (idx < 0 || idx >= filtered.length)
370+
return;
371+
snippetChosen(filtered[idx].originalIndex);
372+
root.close();
373+
}
374+
}

0 commit comments

Comments
 (0)