Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 

 

# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 

# 

# This file is part of qutebrowser. 

# 

# qutebrowser is free software: you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# qutebrowser is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the GNU General Public License 

# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. 

 

"""Completion view for statusbar command section. 

 

Defines a CompletionView which uses CompletionFiterModel and CompletionModel 

subclasses to provide completions. 

""" 

 

from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory 

from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize 

 

from qutebrowser.config import config 

from qutebrowser.completion import completiondelegate 

from qutebrowser.utils import utils, usertypes, debug, log, objreg 

from qutebrowser.commands import cmdexc, cmdutils 

 

 

class CompletionView(QTreeView): 

 

"""The view showing available completions. 

 

Based on QTreeView but heavily customized so root elements show as category 

headers, and children show as flat list. 

 

Attributes: 

pattern: Current filter pattern, used for highlighting. 

_win_id: The ID of the window this CompletionView is associated with. 

_height: The height to use for the CompletionView. 

_height_perc: Either None or a percentage if height should be relative. 

_delegate: The item delegate used. 

_column_widths: A list of column widths, in percent. 

_active: Whether a selection is active. 

 

Signals: 

update_geometry: Emitted when the completion should be resized. 

selection_changed: Emitted when the completion item selection changes. 

""" 

 

# Drawing the item foreground will be done by CompletionItemDelegate, so we 

# don't define that in this stylesheet. 

STYLESHEET = """ 

QTreeView { 

font: {{ conf.fonts.completion.entry }}; 

background-color: {{ conf.colors.completion.even.bg }}; 

alternate-background-color: {{ conf.colors.completion.odd.bg }}; 

outline: 0; 

border: 0px; 

} 

 

QTreeView::item:disabled { 

background-color: {{ conf.colors.completion.category.bg }}; 

border-top: 1px solid 

{{ conf.colors.completion.category.border.top }}; 

border-bottom: 1px solid 

{{ conf.colors.completion.category.border.bottom }}; 

} 

 

QTreeView::item:selected, QTreeView::item:selected:hover { 

border-top: 1px solid 

{{ conf.colors.completion.item.selected.border.top }}; 

border-bottom: 1px solid 

{{ conf.colors.completion.item.selected.border.bottom }}; 

background-color: {{ conf.colors.completion.item.selected.bg }}; 

} 

 

QTreeView:item::hover { 

border: 0px; 

} 

 

QTreeView QScrollBar { 

width: {{ conf.completion.scrollbar.width }}px; 

background: {{ conf.colors.completion.scrollbar.bg }}; 

} 

 

QTreeView QScrollBar::handle { 

background: {{ conf.colors.completion.scrollbar.fg }}; 

border: {{ conf.completion.scrollbar.padding }}px solid 

{{ conf.colors.completion.scrollbar.bg }}; 

min-height: 10px; 

} 

 

QTreeView QScrollBar::sub-line, QScrollBar::add-line { 

border: none; 

background: none; 

} 

""" 

 

update_geometry = pyqtSignal() 

selection_changed = pyqtSignal(str) 

 

def __init__(self, win_id, parent=None): 

super().__init__(parent) 

self.pattern = '' 

self._win_id = win_id 

config.instance.changed.connect(self._on_config_changed) 

 

self._active = False 

 

self._delegate = completiondelegate.CompletionItemDelegate(self) 

self.setItemDelegate(self._delegate) 

self.setStyle(QStyleFactory.create('Fusion')) 

config.set_register_stylesheet(self) 

self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 

self.setHeaderHidden(True) 

self.setAlternatingRowColors(True) 

self.setIndentation(0) 

self.setItemsExpandable(False) 

self.setExpandsOnDoubleClick(False) 

self.setAnimated(False) 

self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 

# WORKAROUND 

# This is a workaround for weird race conditions with invalid 

# item indexes leading to segfaults in Qt. 

# 

# Some background: http://bugs.quassel-irc.org/issues/663 

# The proposed fix there was later reverted because it didn't help. 

self.setUniformRowHeights(True) 

self.hide() 

# FIXME set elidemode 

# https://github.com/qutebrowser/qutebrowser/issues/118 

 

def __repr__(self): 

return utils.get_repr(self) 

 

@pyqtSlot(str) 

def _on_config_changed(self, option): 

if option in ['completion.height', 'completion.shrink']: 

self.update_geometry.emit() 

 

def _resize_columns(self): 

"""Resize the completion columns based on column_widths.""" 

if self.model() is None: 

return 

width = self.size().width() 

column_widths = self.model().column_widths 

pixel_widths = [(width * perc // 100) for perc in column_widths] 

 

delta = self.verticalScrollBar().sizeHint().width() 

156 ↛ 157line 156 didn't jump to line 157, because the condition on line 156 was never true if pixel_widths[-1] > delta: 

pixel_widths[-1] -= delta 

else: 

pixel_widths[-2] -= delta 

 

for i, w in enumerate(pixel_widths): 

assert w >= 0, i 

self.setColumnWidth(i, w) 

 

def _next_idx(self, upwards): 

"""Get the previous/next QModelIndex displayed in the view. 

 

Used by tab_handler. 

 

Args: 

upwards: Get previous item, not next. 

 

Return: 

A QModelIndex. 

""" 

idx = self.selectionModel().currentIndex() 

if not idx.isValid(): 

# No item selected yet 

if upwards: 

return self.model().last_item() 

else: 

return self.model().first_item() 

 

while True: 

idx = self.indexAbove(idx) if upwards else self.indexBelow(idx) 

# wrap around if we arrived at beginning/end 

if not idx.isValid() and upwards: 

return self.model().last_item() 

elif not idx.isValid() and not upwards: 

idx = self.model().first_item() 

self.scrollTo(idx.parent()) 

return idx 

elif idx.parent().isValid(): 

# Item is a real item, not a category header -> success 

return idx 

 

raise utils.Unreachable 

 

def _next_category_idx(self, upwards): 

"""Get the index of the previous/next category. 

 

Args: 

upwards: Get previous item, not next. 

 

Return: 

A QModelIndex. 

""" 

idx = self.selectionModel().currentIndex() 

if not idx.isValid(): 

return self._next_idx(upwards).sibling(0, 0) 

idx = idx.parent() 

direction = -1 if upwards else 1 

while True: 

idx = idx.sibling(idx.row() + direction, 0) 

if not idx.isValid() and upwards: 

# wrap around to the first item of the last category 

return self.model().last_item().sibling(0, 0) 

elif not idx.isValid() and not upwards: 

# wrap around to the first item of the first category 

idx = self.model().first_item() 

self.scrollTo(idx.parent()) 

return idx 

elif idx.isValid() and idx.child(0, 0).isValid(): 

# scroll to ensure the category is visible 

self.scrollTo(idx) 

return idx.child(0, 0) 

 

raise utils.Unreachable 

 

@cmdutils.register(instance='completion', 

modes=[usertypes.KeyMode.command], scope='window') 

@cmdutils.argument('which', choices=['next', 'prev', 'next-category', 

'prev-category']) 

@cmdutils.argument('history', flag='H') 

def completion_item_focus(self, which, history=False): 

"""Shift the focus of the completion menu to another item. 

 

Args: 

which: 'next', 'prev', 'next-category', or 'prev-category'. 

history: Navigate through command history if no text was typed. 

""" 

242 ↛ 243line 242 didn't jump to line 243, because the condition on line 242 was never true if history: 

status = objreg.get('status-command', scope='window', 

window=self._win_id) 

if (status.text() == ':' or status.history.is_browsing() or 

not self._active): 

if which == 'next': 

status.command_history_next() 

return 

elif which == 'prev': 

status.command_history_prev() 

return 

else: 

raise cmdexc.CommandError("Can't combine --history with " 

"{}!".format(which)) 

 

if not self._active: 

return 

 

selmodel = self.selectionModel() 

indices = { 

'next': self._next_idx(upwards=False), 

'prev': self._next_idx(upwards=True), 

'next-category': self._next_category_idx(upwards=False), 

'prev-category': self._next_category_idx(upwards=True), 

} 

idx = indices[which] 

 

if not idx.isValid(): 

return 

 

selmodel.setCurrentIndex( 

idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) 

 

# if the last item is focused, try to fetch more 

if idx.row() == self.model().rowCount(idx.parent()) - 1: 

self.expandAll() 

 

count = self.model().count() 

280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true if count == 0: 

self.hide() 

elif count == 1 and config.val.completion.quick: 

self.hide() 

elif config.val.completion.show == 'auto': 

self.show() 

 

def set_model(self, model): 

"""Switch completion to a new model. 

 

Called from on_update_completion(). 

 

Args: 

model: The model to use. 

""" 

if self.model() is not None and model is not self.model(): 

self.model().deleteLater() 

self.selectionModel().deleteLater() 

 

self.setModel(model) 

 

if model is None: 

self._active = False 

self.hide() 

return 

 

model.setParent(self) 

self._active = True 

self._maybe_show() 

 

self._resize_columns() 

for i in range(model.rowCount()): 

self.expand(model.index(i, 0)) 

 

def set_pattern(self, pattern): 

"""Set the pattern on the underlying model.""" 

if not self.model(): 

return 

self.pattern = pattern 

with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)): 

self.model().set_pattern(pattern) 

self.selectionModel().clear() 

self._maybe_update_geometry() 

self._maybe_show() 

 

def _maybe_show(self): 

if (config.val.completion.show == 'always' and 

self.model().count() > 0): 

self.show() 

else: 

self.hide() 

 

def _maybe_update_geometry(self): 

"""Emit the update_geometry signal if the config says so.""" 

if config.val.completion.shrink: 

self.update_geometry.emit() 

 

@pyqtSlot() 

def on_clear_completion_selection(self): 

"""Clear the selection model when an item is activated.""" 

self.hide() 

selmod = self.selectionModel() 

if selmod is not None: 

selmod.clearSelection() 

selmod.clearCurrentIndex() 

 

def sizeHint(self): 

"""Get the completion size according to the config.""" 

# Get the configured height/percentage. 

confheight = str(config.val.completion.height) 

350 ↛ 354line 350 didn't jump to line 354, because the condition on line 350 was never false if confheight.endswith('%'): 

perc = int(confheight.rstrip('%')) 

height = self.window().height() * perc / 100 

else: 

height = int(confheight) 

# Shrink to content size if needed and shrinking is enabled 

356 ↛ 357line 356 didn't jump to line 357 if config.val.completion.shrink: 

contents_height = ( 

self.viewportSizeHint().height() + 

self.horizontalScrollBar().sizeHint().height()) 

if contents_height <= height: 

height = contents_height 

else: 

contents_height = -1 

# The width isn't really relevant as we're expanding anyways. 

return QSize(-1, height) 

 

def selectionChanged(self, selected, deselected): 

"""Extend selectionChanged to call completers selection_changed.""" 

if not self._active: 

return 

super().selectionChanged(selected, deselected) 

indexes = selected.indexes() 

if not indexes: 

return 

data = str(self.model().data(indexes[0])) 

self.selection_changed.emit(data) 

 

def resizeEvent(self, e): 

"""Extend resizeEvent to adjust column size.""" 

super().resizeEvent(e) 

self._resize_columns() 

 

def showEvent(self, e): 

"""Adjust the completion size and scroll when it's freshly shown.""" 

self.update_geometry.emit() 

scrollbar = self.verticalScrollBar() 

387 ↛ 389line 387 didn't jump to line 389, because the condition on line 387 was never false if scrollbar is not None: 

scrollbar.setValue(scrollbar.minimum()) 

super().showEvent(e) 

 

@cmdutils.register(instance='completion', 

modes=[usertypes.KeyMode.command], scope='window') 

def completion_item_del(self): 

"""Delete the current completion item.""" 

index = self.currentIndex() 

if not index.isValid(): 

raise cmdexc.CommandError("No item selected!") 

self.model().delete_cur_item(index) 

 

@cmdutils.register(instance='completion', 

modes=[usertypes.KeyMode.command], scope='window') 

def completion_item_yank(self, sel=False): 

"""Yank the current completion item into the clipboard. 

 

Args: 

sel: Use the primary selection instead of the clipboard. 

""" 

status = objreg.get('status-command', scope='window', 

window=self._win_id) 

text = status.selectedText() 

if not text: 

index = self.currentIndex() 

413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true if not index.isValid(): 

raise cmdexc.CommandError("No item selected!") 

text = self.model().data(index) 

utils.set_clipboard(text, selection=sel)