Add initial filename completion

This commit is contained in:
Florian Bruhin 2016-10-26 22:53:14 +02:00
parent 6ab51e0b7b
commit 9bdbb257ba
3 changed files with 72 additions and 8 deletions

View File

@ -977,6 +977,7 @@ How many steps to zoom out.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block. |<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
|<<prompt-open-download,prompt-open-download>>|Immediately open a download. |<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|<<repeat-command,repeat-command>>|Repeat the last executed command. |<<repeat-command,repeat-command>>|Repeat the last executed command.
|<<rl-backward-char,rl-backward-char>>|Move back a character. |<<rl-backward-char,rl-backward-char>>|Move back a character.
@ -1229,6 +1230,15 @@ Accept the current prompt.
* +'value'+: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value. * +'value'+: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value.
[[prompt-item-focus]]
=== prompt-item-focus
Syntax: +:prompt-item-focus 'which'+
Shift the focus of the prompt file completion menu to another item.
==== positional arguments
* +'which'+: 'next', 'prev'
[[prompt-open-download]] [[prompt-open-download]]
=== prompt-open-download === prompt-open-download
Syntax: +:prompt-open-download ['cmdline']+ Syntax: +:prompt-open-download ['cmdline']+

View File

@ -1680,6 +1680,8 @@ KEY_DATA = collections.OrderedDict([
('prompt-accept yes', ['y']), ('prompt-accept yes', ['y']),
('prompt-accept no', ['n']), ('prompt-accept no', ['n']),
('prompt-open-download', ['<Ctrl-X>']), ('prompt-open-download', ['<Ctrl-X>']),
('prompt-item-focus prev', ['<Shift-Tab>', '<Up>']),
('prompt-item-focus next', ['<Tab>', '<Down>']),
])), ])),
('command,prompt', collections.OrderedDict([ ('command,prompt', collections.OrderedDict([

View File

@ -24,7 +24,8 @@ import html
import collections import collections
import sip import sip
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QWidgetItem, QFileSystemModel, QTreeView, QLabel, QWidgetItem, QFileSystemModel, QTreeView,
QSizePolicy) QSizePolicy)
@ -264,6 +265,20 @@ class PromptContainer(QWidget):
except UnsupportedOperationError: except UnsupportedOperationError:
pass pass
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt])
@cmdutils.argument('which', choices=['next', 'prev'])
def prompt_item_focus(self, which):
"""Shift the focus of the prompt file completion menu to another item.
Args:
which: 'next', 'prev'
"""
try:
self._prompt.item_focus(which)
except UnsupportedOperationError:
pass
@pyqtSlot(usertypes.Question, bool) @pyqtSlot(usertypes.Question, bool)
def ask_question(self, question, blocking): def ask_question(self, question, blocking):
"""Display a prompt for a given question. """Display a prompt for a given question.
@ -425,6 +440,10 @@ class _BasePrompt(QWidget):
"""Open the download directly if this is a download prompt.""" """Open the download directly if this is a download prompt."""
raise UnsupportedOperationError raise UnsupportedOperationError
def item_focus(self, _which):
"""Switch to next file item if this is a filename prompt.."""
raise UnsupportedOperationError
def _allowed_commands(self): def _allowed_commands(self):
"""Get the commands we could run as response to this message.""" """Get the commands we could run as response to this message."""
raise NotImplementedError raise NotImplementedError
@ -477,7 +496,7 @@ class FilenamePrompt(_BasePrompt):
@pyqtSlot(str) @pyqtSlot(str)
def _set_fileview_root(self, path): def _set_fileview_root(self, path):
"""Set the root path for the file display.""" """Set the root path for the file display."""
if not path.endswith('/'): if not path.endswith('/') or path == '/':
return return
path.rstrip('/') path.rstrip('/')
@ -498,25 +517,33 @@ class FilenamePrompt(_BasePrompt):
self._file_view.setRootIndex(root) self._file_view.setRootIndex(root)
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def _on_clicked(self, index): def _insert_path(self, index, *, clicked=True):
"""Handle a click on an element.""" """Handle an element selection.
Args:
index: The QModelIndex of the selected element.
clicked: Whether the element was clicked.
"""
parts = [] parts = []
cur = index cur = index
while cur.isValid(): while cur.isValid():
parts.append(cur.data()) parts.append(cur.data())
cur = cur.parent() cur = cur.parent()
path = os.path.normpath(os.path.join(*reversed(parts))) + os.sep path = os.path.normpath(os.path.join(*reversed(parts)))
if clicked:
path += os.sep
log.prompt.debug('Clicked {!r} -> {}'.format(parts, path)) log.prompt.debug('Clicked {!r} -> {}'.format(parts, path))
self._lineedit.setText(path) self._lineedit.setText(path)
self._lineedit.setFocus() self._lineedit.setFocus()
# Avoid having a ..-subtree highlighted if clicked:
self._file_view.setCurrentIndex(QModelIndex()) # Avoid having a ..-subtree highlighted
self._file_view.setCurrentIndex(QModelIndex())
def _init_fileview(self): def _init_fileview(self):
self._file_view = QTreeView(self) self._file_view = QTreeView(self)
self._file_model = QFileSystemModel(self) self._file_model = QFileSystemModel(self)
self._file_view.setModel(self._file_model) self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._on_clicked) self._file_view.clicked.connect(self._insert_path)
self._vbox.addWidget(self._file_view) self._vbox.addWidget(self._file_view)
# Only show name # Only show name
self._file_view.setHeaderHidden(True) self._file_view.setHeaderHidden(True)
@ -528,6 +555,31 @@ class FilenamePrompt(_BasePrompt):
self.question.answer = text self.question.answer = text
return True return True
def item_focus(self, which):
# This duplicates some completion code, but I don't see a nicer way...
assert which in ['prev', 'next'], which
selmodel = self._file_view.selectionModel()
first_index = self._file_model.index(0, 0)
last_index = self._file_model.index(self._file_model.rowCount() - 1, 0)
idx = selmodel.currentIndex()
if not idx.isValid():
# No item selected yet
idx = last_index if which == 'prev' else first_index
if which == 'prev':
idx = self._file_view.indexAbove(idx)
else:
idx = self._file_view.indexBelow(idx)
# wrap around if we arrived at beginning/end
if not idx.isValid():
idx = last_index if which == 'prev' else first_index
selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
self._insert_path(idx, clicked=False)
def _allowed_commands(self): def _allowed_commands(self):
"""Get the commands we could run as response to this message.""" """Get the commands we could run as response to this message."""
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')] return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]