-
Notifications
You must be signed in to change notification settings - Fork 0
/
Sticky Searches.py
477 lines (404 loc) · 13.3 KB
/
Sticky Searches.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# -*- coding: utf-8 -*-
"""
Anki Add-on: Sticky Searches
Adds a number of quick-toggles to the Browser search bar that allow
you to preserve specific search parameters across multiple searches,
so that you do not have to type them in repeatedly.
These are either predefined, or dynamically extracted from
the current search.
By default the add-on comes with four toggles:
Static:
- Deck (Hotkey: Alt+D): Limit results to current deck
- Card (Hotkey: Alt+C): Limit results to first card of each note
Dynamic
- Sticky (Hotkey: Alt+S): Will remember the entirety of the current query
- Tags (Hotkey: Alt+T): Limit results to current tag selection
Advanced users can customize these toggles and set up additional ones
by modifying the configuration section below.
Inspired by the following add-ons:
- "Limit searches to current deck" by Damien Elmes
(https://github.com/dae/ankiplugins/blob/master/searchdeck.py)
- "Ignore accents in browser search" by Houssam Salem
(https://github.com/hssm/anki-addons)
This add-on was originally known as "Browser Search Modifiers", but has
since been reworked from the ground up with the kind support of a
fellow Anki user.
Copyright: (c) Glutanimate 2016-2017 <https://glutanimate.com/>
License: GNU AGPLv3 or later <https://www.gnu.org/licenses/agpl.html>
"""
# Do not modify the following line
from __future__ import unicode_literals
############## USER CONFIGURATION START ##############
# CHECKBOX SETTINGS
# You can set-up additional checkboxes here if you know what you are doing
#
# - checkboxes with an empty "value" entry ("value": None) set their sticky
# search tokens dynamically, i.e. by parsing the current query for the supplied
# prefix (e.g. "tag:") and saving all values of that particular prefix
#
# Example of an additional entry for a dynamic "is" checkbox:
#
# "is": {
# "hotkey": "Alt+I",
# "label": "I",
# "icons": "dot",
# "tooltip": "Limit results to current 'is' state",
# "prefix": "is:",
# "value": None
# },
# Example of an additional entry for a static deck checkbox:
#
# "mydeck": {
# "hotkey": "Alt+M",
# "label": "My deck",
# "icons": "deck",
# "tooltip": "Limit results to my deck",
# "prefix": "deck:",
# "value": "'my deck'"
# },
checkboxes = {
"sticky": {
"hotkey": "Alt+S", # Default: Alt+S
"icons": "snowflake",
"tooltip": "Sticky current search",
"prefix": None,
"value": None
},
"deck": {
"hotkey": "Alt+D", # Default: Alt+D
"icons": "deck",
"tooltip": "Limit results to current deck",
"prefix": "deck:",
"value": "current"
},
"tags": {
"hotkey": "Alt+T", # Default: Alt+T
"icons": "tag",
"tooltip": "Limit results to current tag selection",
"prefix": "tag:",
"value": None
},
"card": {
"hotkey": "Alt+C", # Default: Alt+C
"icons": "card",
"tooltip": "Limit results to first card of each note",
"prefix": "card:",
"value": "1"
},
}
# ENABLED CHECKBOXES
# - any additional checkboxes you set up have to be added to this list
# - all checkboxes will be arranged according to the order of this list
# - entries you remove here will stop being displayed
enabled = ("sticky", "deck", "tags", "card")
# OPTIONS
# whether empty queries should clear all sticky parameters
EMPTY_CLEAR = True # Default: True
############## USER CONFIGURATION END ##############
from aqt.qt import *
from aqt import mw
from aqt.browser import Browser
from anki.utils import isWin, isMac
from anki.hooks import wrap
############## VARIABLES START ##############
# Disable sticky tokens for the following searches:
empty_queries = (
_("<type here to search; hit enter to show current deck>"),
"is:current"
)
# Default preferences dictionary
default_prefs = {
"state": {},
"last": {},
}
# Checkbox styling
icon_path = os.path.join(mw.pm.addonFolder(), "sticky_searches", "icons")
def image2url(name):
path = os.path.join(icon_path, "{}.png".format(name))
qurl = QUrl.fromLocalFile(path).toString()
# qss urls use a very strange mix between Windows path
# syntax and URL syntax
# TODO: find a way to use QRessource system instead
if isWin:
to_replace = "file:///"
else:
to_replace = "file://"
return qurl.replace(to_replace, "")
iconsets = {
"snowflake": (
image2url("sticky"),
image2url("unsticky"),
),
"dot": (
image2url("active"),
image2url("inactive"),
),
"tag": (
image2url("tag_active"),
image2url("tag_inactive"),
),
"deck": (
image2url("deck_active"),
image2url("deck_inactive"),
),
"card": (
image2url("card_active"),
image2url("card_inactive"),
),
}
cb_stylesheet = """
QCheckBox::indicator {{
width: 18px;
height: 18px;
}}
QCheckBox::indicator:checked {{
image: url("{0}");
}}
QCheckBox::indicator:unchecked {{
image: url("{1}");
}}
QCheckBox::indicator:checked:hover {{
image: url("{0}");
}}
QCheckBox::indicator:unchecked:hover {{
image: url("{1}");
}}
QCheckBox::indicator:checked:pressed {{
image: url("{0}");
}}
QCheckBox::indicator:unchecked:pressed {{
image: url("{1}");
}}
QCheckBox::indicator:checked:disabled {{
image: url("{0}");
}}
QCheckBox::indicator:unchecked:disabled {{
image: url("{0}");
}}
"""
cb_stylesheet_mac = """
QCheckBox {
margin-left: 6px;
margin-right: 6px;
}
"""
############## VARIABLES END ##############
def tokenize(query):
"""Tokenize search query, adapted from anki.find.Finder"""
inQuote = False
tokens = []
token = ""
for c in query:
# quoted text
if c in ("'", '"'):
if inQuote:
if c == inQuote:
inQuote = False
elif token:
# quotes are allowed to start directly after a :
inQuote = c
else:
inQuote = c
token += c
# separator (space and ideographic space)
elif c in (" ", u'\u3000'):
if inQuote:
token += c
elif token:
# space marks token finished
tokens.append(token)
token = ""
# nesting - modified not to tokenize inside bracket
elif c in ("(", ")"):
if inQuote:
if c == inQuote:
inQuote = False
elif token:
# brackets are allowed to start directly after a " "
if token[-1] in (" ", u'\u3000'):
inQuote = c
else:
inQuote = ")"
token += c
else:
token += c
# if we finished in a token, add it
if token:
tokens.append(token)
return tokens
def uniqueList(seq):
"""Returns unique list while preserving order"""
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
def sortTokens(s):
"""Custom sort function to override token order"""
if s.startswith("deck:"):
return 1
elif s.startswith("tag:"):
return 2
elif s.startswith("is:"):
return 3
elif s.startswith("card:"):
return 4
else:
return s
def onSearch(self, _old=None, reset=True):
"""Intercept search and modify query with our tokens"""
query = unicode(self.form.searchEdit.lineEdit().text()).strip()
sticky = self.cbPrefs["last"].get("sticky", "")
cb_state = self.cbPrefs["state"]
# empty query or all checkboxes inactive
if (query in empty_queries or (EMPTY_CLEAR and query == "")
or not any(i for i in cb_state.values()) ):
return _old(self, reset=reset)
# prepare query
if sticky:
query = query.replace(sticky, "")
# apply tokens and sticky prefix to query
cur_tokens = tokenize(query)
new_tokens = uniqueList(self.cbTokens + cur_tokens)
new_query = " ".join([sticky] + new_tokens)
self.form.searchEdit.lineEdit().setText(new_query)
return _old(self, reset=reset)
def onCbStateChanged(self, state, key):
"""Update persistent search tokens and search bar"""
if state == Qt.Checked:
mode = "add"
else:
mode = "remove"
# Tokenize search and get existing sticky tokens
query = self.form.searchEdit.lineEdit().text().strip()
cb_tokens = self.cbTokens
sticky = self.cbPrefs["last"].get("sticky", "")
if query not in empty_queries or (EMPTY_CLEAR and query != ""):
query_tokens = tokenize(query.replace(sticky, ""))
else:
query_tokens = []
uniq_tokens = list(set(query_tokens) - set(cb_tokens))
# Update tokens and sticky prefix
prefix = checkboxes[key].get("prefix", None)
value = checkboxes[key].get("value", None)
if value:
# static checkbox
token = prefix + value
if mode == "add" and token not in cb_tokens:
cb_tokens.append(token)
elif mode == "remove" and token in cb_tokens:
cb_tokens.remove(token)
elif prefix:
# dynamic checkbox
prefixes = (prefix, "-" + prefix)
if mode == "add":
to_add = [t for t in query_tokens if t.startswith(prefixes)]
if to_add:
self.cbPrefs["last"][key] = to_add
else:
to_add = self.cbPrefs["last"].get(key, None)
if to_add:
cb_tokens = list(set(cb_tokens + to_add))
elif mode == "remove":
cb_tokens = [t for t in cb_tokens if not t.startswith(prefixes)]
elif key == "sticky":
# sticky search toggle
if mode == "add":
sticky = query
for token in cb_tokens:
sticky.replace(token, "")
query = query.replace(sticky, "")
else:
query = query.replace(sticky, "")
sticky = ""
else:
# should not happen
return False
# Save tokens or sticky prefix and update search bar
if key != "sticky":
if query_tokens:
new_query_tokens = uniqueList(cb_tokens + uniq_tokens)
self.form.searchEdit.lineEdit().setText(" ".join(new_query_tokens))
self.cbTokens = sorted(cb_tokens, key=sortTokens)
else:
self.form.searchEdit.lineEdit().setText(sticky + " " + query)
self.cbPrefs["last"]["sticky"] = sticky
# # DEBUG
# print("{} is {}. New tokens:[{}]. New sticky: {}".format(
# key, state, ", ".join(cb_tokens), sticky))
# Save checkbox state and reset
self.cbPrefs["state"][key] = state
self.onSearch()
def onSetupSearch(self):
"""Add new items to the browser UI to allow toggling the add-on."""
layout = self.form.gridLayout
widget = self.form.widget
prefs = mw.pm.profile.get("browserCbs", None)
if not prefs:
prefs = default_prefs
for key in default_prefs:
if key not in prefs:
prefs[key] = default_prefs[key]
# Create check buttons and hotkeys
idx = 1
new_btns = []
tokens = []
for key in enabled:
cb = checkboxes[key]
# Set up tokens
state = prefs["state"].get(key, None)
if not state:
state = prefs["state"]["key"] = Qt.Unchecked
prefix = cb["prefix"]
value = cb["value"]
if state:
if prefix and value:
token = prefix + value
tokens.append(token)
elif prefix:
last = prefs["last"].get(key, None)
if last:
tokens += last
# Set up buttons and hotkeys
hotkey = cb["hotkey"]
if not hotkey: # disable checkbutton
continue
label = cb.get("label", "")
tooltip = cb.get("tooltip", "")
icons = cb.get("icons", None)
b = QCheckBox(label, widget)
if tooltip or hotkey:
cb_tt = "{} ({})".format(tooltip, hotkey)
b.setToolTip(cb_tt)
b.setFocusPolicy(Qt.NoFocus)
stylesheet = ""
if isMac: # fix margins on macOS
stylesheet += cb_stylesheet_mac
if icons: # apply custom icons
iconset = iconsets[icons]
stylesheet += cb_stylesheet.format(*iconset)
if stylesheet:
b.setStyleSheet(stylesheet)
b.setCheckState(state)
b.stateChanged.connect(lambda a, k=key:self.onCbStateChanged(a, k))
new_btns.append(b)
s = QShortcut(QKeySequence(_(cb["hotkey"])),
self, activated=b.toggle)
idx += 1
# Add widgets to gridlayout while restructuring it
items = []
for i in range(0, layout.count()):
item = layout.itemAt(i).widget()
items.append(item)
if item == self.form.searchEdit:
# position our items ofter the search bar
items += new_btns
for i, item in enumerate(items):
layout.addWidget(item, 0, i, 1, 1)
self.cbPrefs = prefs
self.cbTokens = tokens
def onCloseEvent(self, evt):
"""Save configuration on Browser exit"""
mw.pm.profile["browserCbs"] = self.cbPrefs
Browser.onCbStateChanged = onCbStateChanged
Browser.setupSearch = wrap(Browser.setupSearch, onSetupSearch, "after")
Browser.closeEvent = wrap(Browser.closeEvent, onCloseEvent, "before")
Browser.onSearch = wrap(Browser.onSearch, onSearch, "around")