fix(desktop): restore arrow key navigation and place new icons top right

main
reionwong 3 weeks ago
parent f4cda583fd
commit 07a1216787

@ -2143,7 +2143,7 @@ void FolderModel::createActions()
QAction *restore = new QAction(tr("Restore"), this);
QObject::connect(restore, &QAction::triggered, this, &FolderModel::restoreFromTrash);
QAction *showHidden = new QAction(tr("Show hidden files"), this);
QAction *showHidden = new QAction(tr("Show Hidden Files"), this);
QObject::connect(showHidden, &QAction::triggered, this, [=] {
setShowHiddenFiles(!m_showHiddenFiles);
});
@ -2304,8 +2304,9 @@ void FolderModel::updateActions()
if (QAction *showHidden = m_actionCollection.action("showHidden")) {
showHidden->setVisible(!isTrash);
showHidden->setCheckable(true);
showHidden->setChecked(m_showHiddenFiles);
// Plain text instead of a check mark: the label says what the item does.
showHidden->setText(m_showHiddenFiles ? tr("Hide Hidden Files")
: tr("Show Hidden Files"));
}
if (QAction *openInNewWindow = m_actionCollection.action("openInNewWindow")) {

@ -194,10 +194,45 @@ int Positioner::mapFromSource(int row) const
int Positioner::nearestItem(int currentIndex, Qt::ArrowType direction)
{
if (!m_enabled || currentIndex >= rowCount()) {
const int count = rowCount();
if (count <= 0 || currentIndex >= count) {
return -1;
}
// Without stored positions the grid is always packed, so navigation is
// plain index arithmetic along and across the stripes.
if (!m_enabled) {
if (m_perStripe <= 0) {
return -1;
}
if (currentIndex < 0) {
return 0;
}
int next = currentIndex;
switch (direction) {
case Qt::LeftArrow:
next -= 1;
break;
case Qt::RightArrow:
next += 1;
break;
case Qt::UpArrow:
next -= m_perStripe;
break;
case Qt::DownArrow:
next += m_perStripe;
break;
default:
return -1;
}
return (next >= 0 && next < count) ? next : -1;
}
if (currentIndex < 0) {
return firstRow();
}
@ -634,10 +669,23 @@ void Positioner::sourceRowsAboutToBeInserted(const QModelIndex &parent, int star
if (m_deferApplyPositions) {
return;
} else if (m_proxyToSource.isEmpty()) {
beginInsertRows(parent, start, end);
// First icons of an empty desktop. They go on their default cells,
// and the untaken cells before them are inserted as blanks so the
// announced range matches the row count that follows.
QHash<int, int> cells;
int lastNew = -1;
for (int i = start; i <= end; ++i) {
cells.insert(i, defaultCell(i - start));
lastNew = qMax(lastNew, cells.value(i));
}
beginInsertRows(parent, 0, lastNew);
m_beginInsertRowsCalled = true;
initMaps(end + 1);
for (int i = start; i <= end; ++i) {
updateMaps(cells.value(i), i);
}
return;
}
@ -671,14 +719,33 @@ void Positioner::sourceRowsAboutToBeInserted(const QModelIndex &parent, int star
}
if (rest != -1) {
int firstNew = lastRow() + 1;
int remainder = (end - rest);
const int firstNew = lastRow() + 1;
const int remainder = (end - rest);
// Nothing is free inside the existing rows, so the new icons take
// default cells beyond them; every cell up to the last one used is
// part of the insertion, blank or not.
QHash<int, int> occupied = m_proxyToSource;
QList<int> cells;
int lastNew = firstNew + remainder;
for (int i = 0; i <= remainder; ++i) {
int cell = nextDefaultCell(occupied);
if (cell < firstNew) {
cell = firstNew + i;
}
beginInsertRows(parent, firstNew, firstNew + remainder);
occupied.insert(cell, rest + i);
cells.append(cell);
lastNew = qMax(lastNew, cell);
}
beginInsertRows(parent, firstNew, lastNew);
m_beginInsertRowsCalled = true;
for (int i = 0; i <= remainder; ++i) {
updateMaps(firstNew + i, rest + i);
updateMaps(cells.at(i), rest + i);
}
} else {
m_ignoreNextTransaction = true;
@ -822,22 +889,56 @@ void Positioner::sourceLayoutChanged(const QList<QPersistentModelIndex> &parents
emit layoutChanged(QList<QPersistentModelIndex>(), hint);
}
void Positioner::initMaps(int size)
void Positioner::initMaps()
{
m_proxyToSource.clear();
m_sourceToProxy.clear();
if (size == -1) {
size = m_folderModel->rowCount();
const int size = m_folderModel->rowCount();
for (int i = 0; i < size; ++i) {
updateMaps(defaultCell(i), i);
}
}
if (!size) {
return;
// Cell of the nth default slot: the rightmost stripe first, filled from its
// start, then the stripe next to it -- where macOS puts new desktop icons.
// Falls back to plain packing while the grid size is still unknown.
int Positioner::defaultCell(int ordinal) const
{
if (m_perStripe <= 0 || m_stripes <= 0) {
return ordinal;
}
for (int i = 0; i < size; ++i) {
updateMaps(i, i);
// More icons than the grid has room for: the extra ones spill past it,
// where they were piling up before there was a default order.
if (ordinal >= m_perStripe * m_stripes) {
return ordinal;
}
const int stripe = m_stripes - 1 - (ordinal / m_perStripe);
return (stripe * m_perStripe) + (ordinal % m_perStripe);
}
// The first default slot no icon sits on.
int Positioner::nextDefaultCell(const QHash<int, int> &occupied) const
{
if (m_perStripe <= 0 || m_stripes <= 0) {
return -1;
}
const int cellCount = m_perStripe * m_stripes;
for (int ordinal = 0; ordinal < cellCount; ++ordinal) {
const int cell = defaultCell(ordinal);
if (!occupied.contains(cell)) {
return cell;
}
}
return -1;
}
void Positioner::updateMaps(int proxyIndex, int sourceIndex)
@ -873,9 +974,21 @@ int Positioner::lastRow() const
int Positioner::firstFreeRow() const
{
if (!m_proxyToSource.isEmpty()) {
int last = lastRow();
if (m_proxyToSource.isEmpty()) {
return -1;
}
const int last = lastRow();
const int cell = nextDefaultCell(m_proxyToSource);
// Past the rows the model already has: the caller has to announce an
// insertion for it, so leave it to the append path.
if (cell != -1) {
return cell <= last ? cell : -1;
}
// No grid to go by: fall back to the first gap.
if (m_perStripe <= 0 || m_stripes <= 0) {
for (int i = 0; i <= last; ++i) {
if (!m_proxyToSource.contains(i)) {
return i;
@ -951,7 +1064,13 @@ bool Positioner::computeMaps(QHash<int, int> *proxyToSource, QHash<int, int> *so
return last;
};
auto freeCell = [&lastCell](const QHash<int, int> &map) {
auto freeCell = [&lastCell, this](const QHash<int, int> &map) {
const int cell = nextDefaultCell(map);
if (cell != -1) {
return cell;
}
const int last = lastCell(map);
for (int i = 0; i <= last; ++i) {
if (!map.contains(i)) {

@ -100,11 +100,13 @@ private slots:
void sourceLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint);
private:
void initMaps(int size = -1);
void initMaps();
void updateMaps(int proxyIndex, int sourceIndex);
int firstRow() const;
int lastRow() const;
int firstFreeRow() const;
int defaultCell(int ordinal) const;
int nextDefaultCell(const QHash<int, int> &occupied) const;
void applyPositions();
bool computeMaps(QHash<int, int> *proxyToSource, QHash<int, int> *sourceToProxy) const;
bool fitsGrid(int stripe, int pos) const;

@ -63,7 +63,31 @@ Item {
onLayoutKeyChanged: loadLayout()
Component.onCompleted: loadLayout()
Component.onCompleted: {
loadLayout()
takeFocus()
}
// The desktop is built by DesktopView and only then parented into the
// window, so `focus: true` on the view never reaches the focus chain:
// claim it explicitly, and again whenever the window is activated.
function takeFocus() {
// Not while a name is being edited: the editor is a child of the view,
// and taking the focus back would close it.
if (_folderView.editor && _folderView.editor.targetItem)
return
_folderView.forceActiveFocus()
}
Connections {
target: rootItem.Window.window
function onActiveChanged() {
if (rootItem.Window.window.active)
rootItem.takeFocus()
}
}
function loadLayout() {
if (!layoutKey)
@ -145,11 +169,6 @@ Item {
hostWindow: rootItem.Window.window
}
MouseArea {
anchors.fill: parent
onClicked: _folderView.forceActiveFocus()
}
FolderGridView {
id: _folderView
anchors.fill: parent

@ -88,11 +88,9 @@ FishUI.DesktopMenu {
FishUI.MenuItem {
id: showHiddenItem
property var modelAction: control.folderModel ? control.folderModel.action("showHidden") : null
text: modelAction ? modelAction.text : qsTr("Show hidden files")
text: modelAction ? modelAction.text : qsTr("Show Hidden Files")
visible: !control.hasSelection && modelAction && modelAction.visible
enabled: modelAction ? modelAction.enabled : false
checkable: true
checked: modelAction ? modelAction.checked : false
onTriggered: modelAction.trigger()
}

Loading…
Cancel
Save