DefaultFileDialog.qml 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /****************************************************************************
  2. **
  3. ** Copyright (C) 2016 The Qt Company Ltd.
  4. ** Contact: https://www.qt.io/licensing/
  5. **
  6. ** This file is part of the Qt Quick Dialogs module of the Qt Toolkit.
  7. **
  8. ** $QT_BEGIN_LICENSE:LGPL$
  9. ** Commercial License Usage
  10. ** Licensees holding valid commercial Qt licenses may use this file in
  11. ** accordance with the commercial license agreement provided with the
  12. ** Software or, alternatively, in accordance with the terms contained in
  13. ** a written agreement between you and The Qt Company. For licensing terms
  14. ** and conditions see https://www.qt.io/terms-conditions. For further
  15. ** information use the contact form at https://www.qt.io/contact-us.
  16. **
  17. ** GNU Lesser General Public License Usage
  18. ** Alternatively, this file may be used under the terms of the GNU Lesser
  19. ** General Public License version 3 as published by the Free Software
  20. ** Foundation and appearing in the file LICENSE.LGPL3 included in the
  21. ** packaging of this file. Please review the following information to
  22. ** ensure the GNU Lesser General Public License version 3 requirements
  23. ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
  24. **
  25. ** GNU General Public License Usage
  26. ** Alternatively, this file may be used under the terms of the GNU
  27. ** General Public License version 2.0 or (at your option) the GNU General
  28. ** Public license version 3 or any later version approved by the KDE Free
  29. ** Qt Foundation. The licenses are as published by the Free Software
  30. ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
  31. ** included in the packaging of this file. Please review the following
  32. ** information to ensure the GNU General Public License requirements will
  33. ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
  34. ** https://www.gnu.org/licenses/gpl-3.0.html.
  35. **
  36. ** $QT_END_LICENSE$
  37. **
  38. ****************************************************************************/
  39. import QtQml 2.14 as Qml
  40. import QtQuick 2.2
  41. import QtQuick.Controls 1.2
  42. import QtQuick.Controls.Private 1.0 as ControlsPrivate
  43. import QtQuick.Dialogs 1.1
  44. import QtQuick.Dialogs.Private 1.1
  45. import QtQuick.Layouts 1.1
  46. import QtQuick.Window 2.1
  47. import Qt.labs.folderlistmodel 2.1
  48. import Qt.labs.settings 1.0
  49. import "qml"
  50. AbstractFileDialog {
  51. id: root
  52. property Component modelComponent: Component {
  53. FolderListModel {
  54. showFiles: !root.selectFolder
  55. nameFilters: root.selectedNameFilterExtensions
  56. sortField: (view.sortIndicatorColumn === 0 ? FolderListModel.Name :
  57. (view.sortIndicatorColumn === 1 ? FolderListModel.Type :
  58. (view.sortIndicatorColumn === 2 ? FolderListModel.Size : FolderListModel.LastModified)))
  59. sortReversed: view.sortIndicatorOrder === Qt.DescendingOrder
  60. }
  61. }
  62. onVisibleChanged: {
  63. if (visible) {
  64. // If the TableView doesn't have a model yet, create it asynchronously to avoid a UI freeze
  65. if (!view.model) {
  66. var incubator = modelComponent.incubateObject(null, { })
  67. function init(model) {
  68. view.model = model
  69. model.nameFilters = root.selectedNameFilterExtensions
  70. root.folder = model.folder
  71. }
  72. if (incubator.status === Component.Ready) {
  73. init(incubator.object)
  74. } else {
  75. incubator.onStatusChanged = function(status) {
  76. if (status === Component.Ready)
  77. init(incubator.object)
  78. }
  79. }
  80. }
  81. view.needsWidthAdjustment = true
  82. view.selection.clear()
  83. view.focus = true
  84. }
  85. }
  86. Component.onCompleted: {
  87. filterField.currentIndex = root.selectedNameFilterIndex
  88. root.favoriteFolders = settings.favoriteFolders
  89. }
  90. Component.onDestruction: {
  91. settings.favoriteFolders = root.favoriteFolders
  92. }
  93. property Settings settings: Settings {
  94. category: "QQControlsFileDialog"
  95. property alias width: root.width
  96. property alias height: root.height
  97. property alias sidebarWidth: sidebar.width
  98. property alias sidebarSplit: shortcutsScroll.height
  99. property alias sidebarVisible: root.sidebarVisible
  100. property variant favoriteFolders: []
  101. }
  102. property bool showFocusHighlight: false
  103. property SystemPalette palette: SystemPalette { }
  104. property var favoriteFolders: []
  105. function dirDown(path) {
  106. view.selection.clear()
  107. root.folder = "file://" + path
  108. }
  109. function dirUp() {
  110. view.selection.clear()
  111. if (view.model.parentFolder != "")
  112. root.folder = view.model.parentFolder
  113. }
  114. function acceptSelection() {
  115. // transfer the view's selections to QQuickFileDialog
  116. clearSelection()
  117. if (selectFolder && view.selection.count === 0)
  118. addSelection(folder)
  119. else {
  120. view.selection.forEach(function(idx) {
  121. if (view.model.isFolder(idx)) {
  122. if (selectFolder)
  123. addSelection(view.model.get(idx, "fileURL"))
  124. } else {
  125. if (!selectFolder)
  126. addSelection(view.model.get(idx, "fileURL"))
  127. }
  128. })
  129. }
  130. accept()
  131. }
  132. property Action dirUpAction: Action {
  133. text: "\ue810"
  134. shortcut: "Ctrl+U"
  135. onTriggered: dirUp()
  136. tooltip: qsTr("Go up to the folder containing this one")
  137. }
  138. Rectangle {
  139. id: window
  140. implicitWidth: Math.min(root.__maximumDimension, Math.max(Screen.pixelDensity * 100, splitter.implicitWidth))
  141. implicitHeight: Math.min(root.__maximumDimension, Screen.pixelDensity * 80)
  142. color: root.palette.window
  143. Qml.Binding {
  144. target: view.model
  145. property: "folder"
  146. value: root.folder
  147. restoreMode: Binding.RestoreBinding
  148. }
  149. Qml.Binding {
  150. target: currentPathField
  151. property: "text"
  152. value: root.urlToPath(root.folder)
  153. restoreMode: Binding.RestoreBinding
  154. }
  155. Keys.onPressed: {
  156. event.accepted = true
  157. switch (event.key) {
  158. case Qt.Key_Back:
  159. case Qt.Key_Escape:
  160. reject()
  161. break
  162. default:
  163. event.accepted = false
  164. break
  165. }
  166. }
  167. Keys.forwardTo: [view.flickableItem]
  168. SplitView {
  169. id: splitter
  170. x: 0
  171. width: parent.width
  172. anchors.top: titleBar.bottom
  173. anchors.bottom: bottomBar.top
  174. Column {
  175. id: sidebar
  176. Component.onCompleted: if (width < 1) width = sidebarSplitter.maxShortcutWidth
  177. height: parent.height
  178. width: 0 // initial width only; settings and onCompleted will override it
  179. visible: root.sidebarVisible
  180. SplitView {
  181. id: sidebarSplitter
  182. orientation: Qt.Vertical
  183. property real rowHeight: 10
  184. property real maxShortcutWidth: 80
  185. width: parent.width
  186. height: parent.height - favoritesButtons.height
  187. ScrollView {
  188. id: shortcutsScroll
  189. Component.onCompleted: {
  190. if (height < 1)
  191. height = sidebarSplitter.rowHeight * 4.65
  192. Layout.minimumHeight = sidebarSplitter.rowHeight * 2.65
  193. }
  194. height: 0 // initial width only; settings and onCompleted will override it
  195. ListView {
  196. id: shortcutsView
  197. model: __shortcuts.length
  198. anchors.bottomMargin: ControlsPrivate.Settings.hasTouchScreen ? Screen.pixelDensity * 3.5 : anchors.margins
  199. implicitHeight: model.count * sidebarSplitter.rowHeight
  200. delegate: Item {
  201. id: shortcutItem
  202. width: sidebarSplitter.width
  203. height: shortcutLabel.implicitHeight * 1.5
  204. Text {
  205. id: shortcutLabel
  206. text: __shortcuts[index].name
  207. anchors {
  208. verticalCenter: parent.verticalCenter
  209. left: parent.left
  210. right: parent.right
  211. margins: 4
  212. }
  213. elide: Text.ElideLeft
  214. renderType: ControlsPrivate.Settings.isMobile ? Text.QtRendering : Text.NativeRendering
  215. Component.onCompleted: {
  216. sidebarSplitter.rowHeight = parent.height
  217. if (implicitWidth * 1.2 > sidebarSplitter.maxShortcutWidth)
  218. sidebarSplitter.maxShortcutWidth = implicitWidth * 1.2
  219. }
  220. }
  221. MouseArea {
  222. anchors.fill: parent
  223. onClicked: root.folder = __shortcuts[index].url
  224. }
  225. }
  226. }
  227. }
  228. ScrollView {
  229. Layout.minimumHeight: sidebarSplitter.rowHeight * 2.5
  230. ListView {
  231. id: favorites
  232. model: root.favoriteFolders
  233. anchors.topMargin: ControlsPrivate.Settings.hasTouchScreen ? Screen.pixelDensity * 3.5 : anchors.margins
  234. delegate: Item {
  235. width: favorites.width
  236. height: folderLabel.implicitHeight * 1.5
  237. Text {
  238. id: folderLabel
  239. text: root.favoriteFolders[index]
  240. anchors {
  241. verticalCenter: parent.verticalCenter
  242. left: parent.left
  243. right: parent.right
  244. margins: 4
  245. }
  246. elide: Text.ElideLeft
  247. renderType: ControlsPrivate.Settings.isMobile ? Text.QtRendering : Text.NativeRendering
  248. }
  249. Menu {
  250. id: favoriteCtxMenu
  251. title: root.favoriteFolders[index]
  252. MenuItem {
  253. text: qsTr("Remove favorite")
  254. onTriggered: {
  255. root.favoriteFolders.splice(index, 1)
  256. favorites.model = root.favoriteFolders
  257. }
  258. }
  259. }
  260. MouseArea {
  261. id: favoriteArea
  262. anchors.fill: parent
  263. acceptedButtons: Qt.LeftButton | Qt.RightButton
  264. hoverEnabled: true
  265. onClicked: {
  266. if (mouse.button == Qt.LeftButton)
  267. root.folder = root.favoriteFolders[index]
  268. else if (mouse.button == Qt.RightButton)
  269. favoriteCtxMenu.popup()
  270. }
  271. onExited: ControlsPrivate.Tooltip.hideText()
  272. onCanceled: ControlsPrivate.Tooltip.hideText()
  273. Timer {
  274. interval: 1000
  275. running: favoriteArea.containsMouse && !favoriteArea.pressed && folderLabel.truncated
  276. onTriggered: ControlsPrivate.Tooltip.showText(favoriteArea,
  277. Qt.point(favoriteArea.mouseX, favoriteArea.mouseY), urlToPath(root.favoriteFolders[index]))
  278. }
  279. }
  280. }
  281. }
  282. }
  283. }
  284. Row {
  285. id: favoritesButtons
  286. height: plusButton.height + 1
  287. anchors.right: parent.right
  288. anchors.rightMargin: 6
  289. layoutDirection: Qt.RightToLeft
  290. Button {
  291. id: plusButton
  292. style: IconButtonStyle { }
  293. text: "\ue83e"
  294. tooltip: qsTr("Add the current directory as a favorite")
  295. width: height
  296. onClicked: {
  297. root.favoriteFolders.push(root.folder)
  298. favorites.model = root.favoriteFolders
  299. }
  300. }
  301. }
  302. }
  303. TableView {
  304. id: view
  305. sortIndicatorVisible: true
  306. Layout.fillWidth: true
  307. Layout.minimumWidth: 40
  308. property bool needsWidthAdjustment: true
  309. selectionMode: root.selectMultiple ?
  310. (ControlsPrivate.Settings.hasTouchScreen ? SelectionMode.MultiSelection : SelectionMode.ExtendedSelection) :
  311. SelectionMode.SingleSelection
  312. onRowCountChanged: if (needsWidthAdjustment && rowCount > 0) {
  313. resizeColumnsToContents()
  314. needsWidthAdjustment = false
  315. }
  316. model: null
  317. onActivated: if (view.focus) {
  318. if (view.selection.count > 0 && view.model.isFolder(row)) {
  319. dirDown(view.model.get(row, "filePath"))
  320. } else {
  321. root.acceptSelection()
  322. }
  323. }
  324. onClicked: currentPathField.text = view.model.get(row, "filePath")
  325. TableViewColumn {
  326. id: fileNameColumn
  327. role: "fileName"
  328. title: qsTr("Filename")
  329. delegate: Item {
  330. implicitWidth: pathText.implicitWidth + pathText.anchors.leftMargin + pathText.anchors.rightMargin
  331. IconGlyph {
  332. id: fileIcon
  333. x: 4
  334. height: parent.height - 2
  335. unicode: view.model.isFolder(styleData.row) ? "\ue804" : "\ue802"
  336. }
  337. Text {
  338. id: pathText
  339. text: styleData.value
  340. anchors {
  341. left: parent.left
  342. right: parent.right
  343. leftMargin: fileIcon.width + 6
  344. rightMargin: 4
  345. verticalCenter: parent.verticalCenter
  346. }
  347. color: styleData.textColor
  348. elide: Text.ElideRight
  349. renderType: ControlsPrivate.Settings.isMobile ? Text.QtRendering : Text.NativeRendering
  350. }
  351. }
  352. }
  353. TableViewColumn {
  354. role: "fileSuffix"
  355. title: qsTr("Type", "file type (extension)")
  356. // TODO should not need to create a whole new component just to customize the text value
  357. // something like textFormat: function(text) { return view.model.get(styleData.row, "fileIsDir") ? "folder" : text }
  358. delegate: Item {
  359. implicitWidth: sizeText.implicitWidth + sizeText.anchors.leftMargin + sizeText.anchors.rightMargin
  360. Text {
  361. id: sizeText
  362. text: view.model.get(styleData.row, "fileIsDir") ? "folder" : styleData.value
  363. anchors {
  364. left: parent.left
  365. right: parent.right
  366. leftMargin: 4
  367. rightMargin: 4
  368. verticalCenter: parent.verticalCenter
  369. }
  370. color: styleData.textColor
  371. elide: Text.ElideRight
  372. renderType: ControlsPrivate.Settings.isMobile ? Text.QtRendering : Text.NativeRendering
  373. }
  374. }
  375. }
  376. TableViewColumn {
  377. role: "fileSize"
  378. title: qsTr("Size", "file size")
  379. horizontalAlignment: Text.AlignRight
  380. }
  381. TableViewColumn { id: modifiedColumn; role: "fileModified" ; title: qsTr("Modified", "last-modified time") }
  382. TableViewColumn { id: accessedColumn; role: "fileAccessed" ; title: qsTr("Accessed", "last-accessed time") }
  383. }
  384. }
  385. ToolBar {
  386. id: titleBar
  387. RowLayout {
  388. anchors.fill: parent
  389. ToolButton {
  390. action: dirUpAction
  391. style: IconButtonStyle { }
  392. Layout.maximumWidth: height * 1.5
  393. }
  394. TextField {
  395. id: currentPathField
  396. Layout.fillWidth: true
  397. function doAccept() {
  398. root.clearSelection()
  399. if (root.addSelection(root.pathToUrl(text)))
  400. root.accept()
  401. else
  402. root.folder = root.pathFolder(text)
  403. }
  404. onAccepted: doAccept()
  405. }
  406. }
  407. }
  408. Item {
  409. id: bottomBar
  410. width: parent.width
  411. height: buttonRow.height + buttonRow.spacing * 2
  412. anchors.bottom: parent.bottom
  413. Row {
  414. id: buttonRow
  415. anchors.right: parent.right
  416. anchors.rightMargin: spacing
  417. anchors.verticalCenter: parent.verticalCenter
  418. spacing: 4
  419. Button {
  420. id: toggleSidebarButton
  421. checkable: true
  422. style: IconButtonStyle { }
  423. text: "\u25E7"
  424. height: cancelButton.height
  425. width: height
  426. checked: root.sidebarVisible
  427. onClicked: {
  428. root.sidebarVisible = !root.sidebarVisible
  429. }
  430. }
  431. ComboBox {
  432. id: filterField
  433. model: root.nameFilters
  434. visible: !selectFolder
  435. width: bottomBar.width - toggleSidebarButton.width - cancelButton.width - okButton.width - parent.spacing * 6
  436. anchors.verticalCenter: parent.verticalCenter
  437. onCurrentTextChanged: {
  438. root.selectNameFilter(currentText)
  439. if (view.model)
  440. view.model.nameFilters = root.selectedNameFilterExtensions
  441. }
  442. }
  443. Button {
  444. id: cancelButton
  445. text: qsTr("Cancel")
  446. onClicked: root.reject()
  447. }
  448. Button {
  449. id: okButton
  450. text: root.selectFolder ? qsTr("Choose") : (selectExisting ? qsTr("Open") : qsTr("Save"))
  451. onClicked: {
  452. if (view.model.isFolder(view.currentRow) && !selectFolder)
  453. dirDown(view.model.get(view.currentRow, "filePath"))
  454. else if (!(root.selectExisting))
  455. currentPathField.doAccept()
  456. else
  457. root.acceptSelection()
  458. }
  459. }
  460. }
  461. }
  462. }
  463. }