feat(filemanager): refine sidebar and navigation layout

main
reionwong 3 weeks ago
parent 4020ae4d18
commit 30012a0582

@ -56,6 +56,16 @@ bool PathHistory::isEmpty()
return m_prevHistory.isEmpty();
}
bool PathHistory::canGoBack() const
{
return m_prevHistory.length() >= 2;
}
bool PathHistory::canGoForward() const
{
return !m_postHistory.isEmpty();
}
QUrl PathHistory::posteriorPath()
{
if (m_postHistory.isEmpty())

@ -40,6 +40,9 @@ public:
bool isEmpty();
bool canGoBack() const;
bool canGoForward() const;
QUrl posteriorPath();
QUrl previousPath();

@ -458,6 +458,8 @@ void FolderModel::setUrl(const QString &url)
// Refresh this directory.
if (url == m_url) {
refresh();
emit canGoBackChanged();
emit canGoForwardChanged();
return;
}
@ -479,6 +481,8 @@ void FolderModel::setUrl(const QString &url)
emit urlChanged();
emit resolvedUrlChanged();
emit canGoBackChanged();
emit canGoForwardChanged();
}
QUrl FolderModel::resolvedUrl() const
@ -637,6 +641,16 @@ bool FolderModel::dragging() const
return m_dragInProgress;
}
bool FolderModel::canGoBack() const
{
return m_pathHistory.canGoBack();
}
bool FolderModel::canGoForward() const
{
return m_pathHistory.canGoForward();
}
bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const
{
KFileItem item = dirModel->itemForIndex(index);

@ -71,6 +71,8 @@ class FolderModel : public QSortFilterProxyModel, public QQmlParserStatus
Q_PROPERTY(int archiveProgress READ archiveProgress NOTIFY archiveProgressChanged)
Q_PROPERTY(QString archiveOperation READ archiveOperation NOTIFY archiveOperationChanged)
Q_PROPERTY(QString archiveCurrentFile READ archiveCurrentFile NOTIFY archiveCurrentFileChanged)
Q_PROPERTY(bool canGoBack READ canGoBack NOTIFY canGoBackChanged)
Q_PROPERTY(bool canGoForward READ canGoForward NOTIFY canGoForwardChanged)
public:
enum DataRole {
@ -160,6 +162,9 @@ public:
bool dragging() const;
bool canGoBack() const;
bool canGoForward() const;
bool isDir(const QModelIndex &index, const KDirModel *dirModel) const;
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
@ -263,6 +268,8 @@ signals:
void filterModeChanged();
void requestRename();
void draggingChanged();
void canGoBackChanged();
void canGoForwardChanged();
void viewAdapterChanged();
void isDesktopChanged();
void selectionCountChanged();

@ -38,8 +38,13 @@ Item {
Rectangle {
id: _background
anchors.fill: parent
radius: FishUI.Theme.smallRadius
color: _mouseArea.pressed ? pressedColor : _mouseArea.containsMouse ? control.hoveredColor : control.backgroundColor
// Same inset as FishUI's window buttons, so both draw the same circle.
anchors.margins: Math.round(control.width * 0.1)
radius: width / 2
color: !control.enabled ? "transparent"
: _mouseArea.pressed ? pressedColor
: _mouseArea.containsMouse ? control.hoveredColor
: control.backgroundColor
}
Image {
@ -50,11 +55,13 @@ Item {
sourceSize: Qt.size(width, height)
smooth: false
antialiasing: true
opacity: control.enabled ? 1 : 0.35
}
MouseArea {
id: _mouseArea
anchors.fill: parent
enabled: control.enabled
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onClicked: control.clicked()

@ -33,8 +33,11 @@ Item {
property alias currentUrl: dirModel.url
property alias model: dirModel
property alias canGoBack: dirModel.canGoBack
property alias canGoForward: dirModel.canGoForward
property Item currentView: _viewLoader.item
property int statusBarHeight: 22
property int headerHeight: 0
property int bottomNavigationHeight: 0
signal requestPathEditor()
@ -200,7 +203,9 @@ Item {
ColumnLayout {
anchors.fill: parent
anchors.bottomMargin: 2
// Same breathing room the sidebar leaves between its title and its list.
anchors.topMargin: folderPage.headerHeight
anchors.bottomMargin: 2 + folderPage.bottomNavigationHeight
spacing: 0
Loader {
@ -221,82 +226,6 @@ Item {
shortCut.install(_viewLoader.item)
}
}
Item {
visible: true
height: statusBarHeight
}
}
Item {
id: _statusBar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: statusBarHeight
z: 999
// Rectangle {
// anchors.fill: parent
// color: FishUI.Theme.backgroundColor
// opacity: 0.7
// }
MouseArea {
anchors.fill: parent
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: FishUI.Units.smallSpacing
anchors.rightMargin: FishUI.Units.smallSpacing
// anchors.bottomMargin: 1
spacing: FishUI.Units.largeSpacing
Label {
Layout.alignment: Qt.AlignLeft
font.pointSize: 10
text: dirModel.count === 1 ? qsTr("%1 item").arg(dirModel.count)
: qsTr("%1 items").arg(dirModel.count)
}
Label {
Layout.alignment: Qt.AlignLeft
font.pointSize: 10
text: qsTr("%1 selected").arg(dirModel.selectionCount)
visible: dirModel.selectionCount >= 1
}
FishUI.BusyIndicator {
id: _busyIndicator
Layout.alignment: Qt.AlignLeft
height: statusBarHeight
width: height
running: visible
visible: dirModel.status === FM.FolderModel.Listing
}
Label {
text: dirModel.selectedItemSize
visible: dirModel.url !== "trash:///"
}
Item {
Layout.fillWidth: true
}
Button {
id: _emptyTrashBtn
implicitHeight: statusBarHeight
text: qsTr("Empty Trash")
font.pointSize: 10
onClicked: dirModel.emptyTrash()
visible: dirModel.url === "trash:///"
&& _viewLoader.item
&& _viewLoader.item.count > 0
focusPolicy: Qt.NoFocus
}
}
}
function rename() {

@ -29,11 +29,30 @@ Item {
id: control
property string url: ""
property string currentName: nameForUrl(url)
signal itemClicked(string path)
signal editorAccepted(string path)
// The path bar is part of the title bar. Start the Wayland move only
function nameForUrl(value) {
if (!value)
return ""
if (value.indexOf("trash:/") === 0)
return qsTr("Trash")
var path = value.toString()
if (path.indexOf("file://") === 0)
path = path.substring(7)
while (path.length > 1 && path.endsWith("/"))
path = path.substring(0, path.length - 1)
var separator = path.lastIndexOf("/")
return separator >= 0 ? (path.substring(separator + 1) || "/") : path
}
// The path bar is part of the navigation bar. Start the Wayland move only
// after a drag begins, so a single click can open the path editor.
property bool _dragged: false
property real _pressX: 0

@ -25,23 +25,20 @@ import QtQuick.Window 2.12
import FishUI 1.0 as FishUI
import Cutefish.FileManager 1.0
ListView {
Item {
id: sideBar
signal clicked(string path)
signal openInNewWindow(string path)
implicitWidth: 190
// Rounding to whole *logical* pixels is not enough at a fractional scale:
// at 150% a 37px row pitch is 55.5 device px, so every other row lands on
// a half pixel and its icon is resampled.
readonly property real dpr: FishUI.Dpi.ratio
function snap(v) {
return Math.round(v * sideBar.dpr) / sideBar.dpr
}
property string title
// The header aligns its own contents to this label's text line.
property alias titleItem: titleLabel
property alias currentIndex: listView.currentIndex
property alias count: listView.count
property alias model: listView.model
FishUI.WheelHandler {
target: sideBar
}
signal clicked(string path)
signal openInNewWindow(string path)
Fm {
id: _fm
@ -52,170 +49,219 @@ ListView {
onDeviceSetupDone: sideBar.clicked(filePath) //
}
model: placesModel
clip: true
// Not leftMargin/rightMargin: Qt rounds the content item's position to a
// whole logical pixel, so a snapped fractional margin never takes effect.
// The inset is applied inside the rows instead.
readonly property real sideInset: sideBar.snap(FishUI.Units.smallSpacing * 1.5)
bottomMargin: sideBar.snap(FishUI.Units.smallSpacing)
spacing: sideBar.snap(FishUI.Units.smallSpacing / 2)
Rectangle {
anchors.fill: parent
color: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.5)
: Qt.darker(FishUI.Theme.backgroundColor, 1.05)
opacity: 0.7
ScrollBar.vertical: ScrollBar {
bottomPadding: FishUI.Units.smallSpacing
}
section.property: "category"
section.delegate: Item {
width: ListView.view.width
height: sideBar.snap(FishUI.Units.fontMetrics.height + FishUI.Units.largeSpacing + FishUI.Units.smallSpacing)
Text {
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: Qt.application.layoutDirection === Qt.RightToLeft
? 0 : sideBar.snap(sideBar.sideInset + FishUI.Units.smallSpacing)
anchors.rightMargin: FishUI.Units.smallSpacing
anchors.topMargin: FishUI.Units.largeSpacing
anchors.bottomMargin: FishUI.Units.smallSpacing
color: FishUI.Theme.textColor
font.pointSize: 9
font.bold: true
text: section
Behavior on color {
ColorAnimation {
duration: 250
easing.type: Easing.Linear
}
}
}
delegate: Item {
id: _item
width: ListView.view.width
height: sideBar.snap(FishUI.Units.fontMetrics.height + FishUI.Units.largeSpacing * 1.5)
property bool checked: sideBar.currentIndex === index
property color hoveredColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.1)
: Qt.darker(FishUI.Theme.backgroundColor, 1.1)
MouseArea {
id: _mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
if (model.isDevice && model.setupNeeded)
placesModel.requestSetup(index)
else
sideBar.clicked(model.path ? model.path : model.url)
} else if (mouse.button === Qt.RightButton) {
_menu.popup()
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
FishUI.DesktopMenu {
id: _menu
Label {
id: titleLabel
text: sideBar.title
color: root.active ? FishUI.Theme.textColor : FishUI.Theme.disabledTextColor
Layout.preferredHeight: root.header.height
leftPadding: FishUI.Units.largeSpacing + FishUI.Units.smallSpacing
rightPadding: FishUI.Units.largeSpacing + FishUI.Units.smallSpacing
topPadding: FishUI.Units.smallSpacing
bottomPadding: 0
font.pointSize: 12
}
FishUI.MenuItem {
text: qsTr("Open")
ListView {
id: listView
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: FishUI.Units.smallSpacing
clip: true
model: placesModel
// Rounding to whole *logical* pixels is not enough at a fractional scale:
// at 150% a 37px row pitch is 55.5 device px, so every other row lands on
// a half pixel and its icon is resampled.
readonly property real dpr: FishUI.Dpi.ratio
function snap(v) {
return Math.round(v * listView.dpr) / listView.dpr
}
onTriggered: {
if (model.isDevice && model.setupNeeded)
placesModel.requestSetup(index)
else
sideBar.clicked(model.path ? model.path : model.url)
}
FishUI.WheelHandler {
target: listView
}
FishUI.MenuItem {
text: qsTr("Open in new window")
// Not leftMargin/rightMargin: Qt rounds the content item's position to a
// whole logical pixel, so a snapped fractional margin never takes effect.
// The inset is applied inside the rows instead.
readonly property real sideInset: listView.snap(FishUI.Units.smallSpacing * 1.5)
onTriggered: {
sideBar.openInNewWindow(model.path ? model.path : model.url)
}
}
bottomMargin: listView.snap(FishUI.Units.smallSpacing)
spacing: 2
FishUI.MenuSeparator {
Layout.fillWidth: true
visible: _ejectMenuItem.visible || _umountMenuItem.visible
ScrollBar.vertical: ScrollBar {
bottomPadding: FishUI.Units.smallSpacing
}
FishUI.MenuItem {
id: _ejectMenuItem
text: qsTr("Eject")
visible: model.isDevice &&
!model.setupNeeded &&
model.isOpticalDisc &&
!model.url.toString() === _fm.rootPath()
onTriggered: {
placesModel.requestEject(index)
section.property: "category"
section.delegate: Item {
width: ListView.view.width
height: listView.snap(FishUI.Units.fontMetrics.height + FishUI.Units.largeSpacing + FishUI.Units.smallSpacing)
Text {
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: Qt.application.layoutDirection === Qt.RightToLeft
? 0 : listView.snap(listView.sideInset + FishUI.Units.smallSpacing)
anchors.rightMargin: FishUI.Units.smallSpacing
anchors.topMargin: FishUI.Units.largeSpacing
anchors.bottomMargin: FishUI.Units.smallSpacing
color: FishUI.Theme.textColor
font.pointSize: 9
font.bold: true
text: section
}
}
FishUI.MenuItem {
id: _umountMenuItem
text: qsTr("Unmount")
visible: model.isDevice &&
!model.setupNeeded &&
!model.isOpticalDisc &&
!model.url.toString() === _fm.rootPath()
delegate: Item {
id: _item
width: ListView.view.width
height: listView.snap(FishUI.Units.fontMetrics.height + FishUI.Units.largeSpacing * 1.5)
property bool checked: sideBar.currentIndex === index
property color hoveredColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.1)
: Qt.darker(FishUI.Theme.backgroundColor, 1.1)
MouseArea {
id: _mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
if (model.isDevice && model.setupNeeded)
placesModel.requestSetup(index)
else
sideBar.clicked(model.path ? model.path : model.url)
} else if (mouse.button === Qt.RightButton) {
_menu.popup()
}
}
}
onTriggered: {
placesModel.requestTeardown(index)
FishUI.DesktopMenu {
id: _menu
FishUI.MenuItem {
text: qsTr("Open")
onTriggered: {
if (model.isDevice && model.setupNeeded)
placesModel.requestSetup(index)
else
sideBar.clicked(model.path ? model.path : model.url)
}
}
FishUI.MenuItem {
text: qsTr("Open in new window")
onTriggered: {
sideBar.openInNewWindow(model.path ? model.path : model.url)
}
}
FishUI.MenuSeparator {
Layout.fillWidth: true
visible: _ejectMenuItem.visible || _umountMenuItem.visible
}
FishUI.MenuItem {
id: _ejectMenuItem
text: qsTr("Eject")
visible: model.isDevice &&
!model.setupNeeded &&
model.isOpticalDisc &&
!model.url.toString() === _fm.rootPath()
onTriggered: {
placesModel.requestEject(index)
}
}
FishUI.MenuItem {
id: _umountMenuItem
text: qsTr("Unmount")
visible: model.isDevice &&
!model.setupNeeded &&
!model.isOpticalDisc &&
!model.url.toString() === _fm.rootPath()
onTriggered: {
placesModel.requestTeardown(index)
}
}
}
}
}
Rectangle {
x: sideBar.sideInset
width: _item.width - sideBar.sideInset * 2
height: parent.height
radius: FishUI.Theme.mediumRadius
color: _mouseArea.pressed ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, FishUI.Theme.darkMode ? 0.05 : 0.1) :
_mouseArea.containsMouse && !checked ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, FishUI.Theme.darkMode ? 0.1 : 0.05) :
checked ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, 0.05) : "transparent"
smooth: true
}
Rectangle {
x: listView.sideInset
width: _item.width - listView.sideInset * 2
height: parent.height
radius: FishUI.Theme.mediumRadius
color: _mouseArea.pressed ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, FishUI.Theme.darkMode ? 0.05 : 0.1) :
_mouseArea.containsMouse && !checked ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, FishUI.Theme.darkMode ? 0.1 : 0.05) :
checked ? Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, 0.05) : "transparent"
smooth: true
}
// Positioned by hand rather than with a RowLayout: AlignVCenter computes
// (height - 22) / 2, which lands on a half device pixel even when the
// row height itself is snapped.
FishUI.IconItem {
id: _icon
x: sideBar.snap(sideBar.sideInset + FishUI.Units.smallSpacing)
y: sideBar.snap((_item.height - height) / 2)
width: sideBar.snap(22)
height: width
source: "qrc:/images/" + model.iconPath
color: checked ? FishUI.Theme.highlightColor : FishUI.Theme.textColor
}
// Positioned by hand rather than with a RowLayout: AlignVCenter computes
// (height - 22) / 2, which lands on a half device pixel even when the
// row height itself is snapped.
FishUI.IconItem {
id: _icon
x: listView.snap(listView.sideInset + FishUI.Units.smallSpacing)
y: listView.snap((_item.height - height) / 2)
width: listView.snap(22)
height: width
source: "qrc:/images/" + model.iconPath
color: checked ? FishUI.Theme.highlightColor : FishUI.Theme.textColor
}
Label {
id: _label
x: sideBar.snap(_icon.x + _icon.width + FishUI.Units.smallSpacing)
y: sideBar.snap((_item.height - height) / 2)
width: _item.width - x - sideBar.snap(FishUI.Units.smallSpacing)
text: model.name
color: checked ? FishUI.Theme.highlightColor : FishUI.Theme.textColor
elide: Text.ElideRight
Label {
id: _label
x: listView.snap(_icon.x + _icon.width + FishUI.Units.smallSpacing)
y: listView.snap((_item.height - height) / 2)
width: _item.width - x - listView.snap(FishUI.Units.smallSpacing)
text: model.name
color: checked ? FishUI.Theme.highlightColor : FishUI.Theme.textColor
elide: Text.ElideRight
}
}
}
}
function updateSelection(path) {
sideBar.currentIndex = -1
listView.currentIndex = -1
for (var i = 0; i < sideBar.count; ++i) {
if (path === sideBar.model.get(i).path ||
path === sideBar.model.get(i).url) {
sideBar.currentIndex = i
for (var i = 0; i < listView.count; ++i) {
if (path === listView.model.get(i).path ||
path === listView.model.get(i).url) {
listView.currentIndex = i
break
}
}

@ -35,7 +35,8 @@ FishUI.Window {
title: qsTr("File Manager")
background.opacity: 1
header.height: 36 + FishUI.Units.largeSpacing
header.height: 40
contentTopMargin: 0
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
LayoutMirroring.childrenInherit: true
@ -62,41 +63,57 @@ FishUI.Window {
headerItem: Item {
RowLayout {
id: _headerRow
anchors.fill: parent
anchors.leftMargin: FishUI.Units.smallSpacing * 1.5
anchors.rightMargin: FishUI.Units.smallSpacing * 1.5
anchors.topMargin: FishUI.Units.smallSpacing * 1.5
anchors.bottomMargin: FishUI.Units.smallSpacing * 1.5
anchors.leftMargin: _sideBar.width + FishUI.Units.smallSpacing * 1.5
anchors.rightMargin: FishUI.Units.smallSpacing
spacing: FishUI.Units.smallSpacing
readonly property int buttonSize: 31
FontMetrics {
id: _nameMetrics
font: _folderName.font
}
IconButton {
Layout.fillHeight: true
implicitWidth: height
Layout.alignment: root.windowButtonsAlignment
Layout.topMargin: root.windowButtonsTopMargin
Layout.preferredWidth: _headerRow.buttonSize
Layout.preferredHeight: _headerRow.buttonSize
enabled: _folderPage.canGoBack
source: FishUI.Theme.darkMode ? "qrc:/images/dark/go-previous.svg"
: "qrc:/images/light/go-previous.svg"
onClicked: _folderPage.goBack()
}
IconButton {
Layout.fillHeight: true
implicitWidth: height
Layout.alignment: root.windowButtonsAlignment
Layout.topMargin: root.windowButtonsTopMargin
Layout.preferredWidth: _headerRow.buttonSize
Layout.preferredHeight: _headerRow.buttonSize
enabled: _folderPage.canGoForward
source: FishUI.Theme.darkMode ? "qrc:/images/dark/go-next.svg"
: "qrc:/images/light/go-next.svg"
onClicked: _folderPage.goForward()
}
PathBar {
id: _pathBar
Label {
id: _folderName
Layout.fillWidth: true
Layout.fillHeight: true
onItemClicked: _folderPage.openUrl(path)
onEditorAccepted: _folderPage.openUrl(path)
leftPadding: root.windowButtonsTopMargin
topPadding: root.windowButtonsTopMargin / 2
color: root.active ? FishUI.Theme.textColor : FishUI.Theme.disabledTextColor
font.pointSize: 11
text: _pathBar.currentName
}
IconButton {
Layout.fillHeight: true
implicitWidth: height
Layout.alignment: root.windowButtonsAlignment
Layout.topMargin: root.windowButtonsTopMargin
Layout.preferredWidth: _headerRow.buttonSize
Layout.preferredHeight: _headerRow.buttonSize
property var gridSource: FishUI.Theme.darkMode ? "qrc:/images/dark/grid.svg" : "qrc:/images/light/grid.svg"
property var listSource: FishUI.Theme.darkMode ? "qrc:/images/dark/list.svg" : "qrc:/images/light/list.svg"
@ -117,7 +134,7 @@ FishUI.Window {
SideBar {
id: _sideBar
Layout.fillHeight: true
width: 180 + FishUI.Units.largeSpacing
title: root.title
onClicked: _folderPage.openUrl(path)
onOpenInNewWindow: _folderPage.model.openInNewWindow(path)
}
@ -126,6 +143,8 @@ FishUI.Window {
id: _folderPage
Layout.fillWidth: true
Layout.fillHeight: true
headerHeight: root.header.height
bottomNavigationHeight: root.header.height
onCurrentUrlChanged: {
_sideBar.updateSelection(currentUrl)
_pathBar.updateUrl(currentUrl)
@ -135,4 +154,40 @@ FishUI.Window {
}
}
}
Item {
id: _navigationBar
x: _folderPage.x
width: _folderPage.width
height: root.header.height
anchors.bottom: parent.bottom
z: 3
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 1
color: Qt.rgba(FishUI.Theme.textColor.r,
FishUI.Theme.textColor.g,
FishUI.Theme.textColor.b, FishUI.Theme.darkMode ? 0.16 : 0.12)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: FishUI.Units.smallSpacing * 1.5
anchors.rightMargin: FishUI.Units.smallSpacing * 1.5
anchors.topMargin: FishUI.Units.smallSpacing
anchors.bottomMargin: FishUI.Units.smallSpacing
spacing: 0
PathBar {
id: _pathBar
Layout.fillWidth: true
Layout.fillHeight: true
onItemClicked: _folderPage.openUrl(path)
onEditorAccepted: _folderPage.openUrl(path)
}
}
}
}

Loading…
Cancel
Save