Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: special/cascade
base: d4344b73b2
...
head fork: special/cascade
compare: 0a050e3595
  • 10 commits
  • 11 files changed
  • 0 commit comments
  • 1 contributor
Commits on Apr 20, 2012
John Brooks Add QML files to OTHER_FILES so they appear in Qt Creator 3230890
John Brooks Keep loading images to fill the screen in FolderLayout fca5556
John Brooks Try putting a file count in the filmstrip for folders 8b3c615
Commits on Apr 23, 2012
John Brooks Refactor FolderLayout logic for clarity and introduce big items
Experimental changes, always.
772e15a
John Brooks Add file/folder counting functions to File 248be29
John Brooks Show a small header next to the big item in FolderLayout 42831dc
John Brooks Draw the big item as soon as the first two rows are done a0a6f87
John Brooks Minor appearance tweaks 0e47ead
John Brooks Save the position when leaving a folder for use if immediately re-ent…
…ered

This is rudimentary, but works for the immediate need.
594824f
Commits on Apr 29, 2012
John Brooks Improve the folder layout logic for big items
Attempt to size the big item so that the long edge fills 55% of the
area, and the short edge is by aspect. Lay out items as normal until
the desired height for the big item is met or exceeded, and set its
height accordingly (cropping as necessary to keep width).

If the height would, including the current row, exceed 75% of the
area, do not include the big item on that row, to avoid taking over
too much of the view area.

There is obviously not ideal; there's a lot of room for improvement,
especially if the big item width weren't static, but that may be too
complicated.

Could potentially be improved by sizing/splitting small items
adjacent to the big item according to what will best serve the big
item (within constraints), but that would be complicated to design.

The "height is met or exceeded" rule may be better comparing the
delta between the last row and the current row and choosing whichever
will result in a better big item size; right now, it prefers the
larger, but that comes at the cost of significant cropping.
0a050e3
5 cascade.pro
View
@@ -32,3 +32,8 @@ HEADERS += \
RESOURCES += \
res/res.qrc \
qml/qml.qrc
+
+OTHER_FILES += qml/cascade/*.qml \
+ qml/cascade/*.js \
+ qml/cascade/folderlayout.js \
+ qml/cascade/util.js
2  qml/cascade/Filmstrip.qml
View
@@ -104,12 +104,12 @@ ListView {
Text {
anchors.fill: parent
+ anchors.margins: 3
visible: model.file.isDirectory
text: model.file.name
color: "white"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
- textFormat: Text.PlainText
wrapMode: Text.Wrap
}
}
136 qml/cascade/FolderLayout.qml
View
@@ -1,13 +1,15 @@
import cascade 0.0
import QtQuick 1.1
+import "folderlayout.js" as Layout
+import "util.js" as Util
Item {
id: layoutRoot
property QtObject directory
- property int padding: 8
- property int desiredHeight: 250
property list<Item> items
+ property bool hasMoreFiles: false
+ property int lastItemPos: 0
Component {
id: imageItem
@@ -53,17 +55,63 @@ Item {
}
}
+ Rectangle {
+ anchors.left: (items.length > 0) ? items[0].right : undefined
+ anchors.right: parent.right
+ y: 8
+ height: Math.ceil(text.paintedHeight) + 4
+ color: "black"
+ z: 1
+
+ onHeightChanged: Layout.headerYOffset = height
+
+ Text {
+ id: text
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.leftMargin: 8
+ anchors.right: parent.right
+ anchors.rightMargin: 8
+ color: "#dddddd"
+ horizontalAlignment: Text.AlignRight
+ elide: Text.ElideRight
+ text: Util.pluralize(directory.totalFileCount, " file") + ", " +
+ Util.pluralize(directory.totalFolderCount, " folder")
+ }
+ }
+
onDirectoryChanged: {
for (var i = 0; i < items.length; i++)
items[i].destroy()
items = []
+ hasMoreFiles = false
+ lastItemPos = 0
if (directory == null)
return
+ populate(0, 16)
+ layout()
+ layoutTimer.stop()
+ }
+
+ onHeightChanged: layout()
+ onWidthChanged: layout()
+
+ Timer {
+ id: layoutTimer
+ interval: 1
+
+ onTriggered: layout()
+ }
+
+ function populate(start, count) {
var tmpItems = new Array()
+ for (var i = 0; i < items.length; i++)
+ tmpItems.push(items[i])
+ var more = true
- for (var i = 0; i < 16; i++) {
+ for (var i = start; i < (start+count); i++) {
var file = directory.at(i)
while (file && file.isDirectory) {
file = file.nextIn
@@ -76,30 +124,22 @@ Item {
i++
}
- if (file == null)
+ if (file === null) {
+ more = false
break
+ }
var obj = imageItem.createObject(layoutRoot, {
"file": file,
"visible": false
});
tmpItems.push(obj)
+ lastItemPos = i
}
items = tmpItems
- layout()
- layoutTimer.stop()
- }
-
- onWidthChanged: layout()
- onPaddingChanged: scheduleLayout()
- onDesiredHeightChanged: scheduleLayout()
-
- Timer {
- id: layoutTimer
- interval: 1
-
- onTriggered: layout()
+ hasMoreFiles = more
+ return more
}
function scheduleLayout() {
@@ -107,70 +147,14 @@ Item {
}
function layout(first, count) {
- if (!items.length)
- return
+ first = typeof first !== 'undefined' ? first : 0
+ count = typeof count !== 'undefined' ? count : items.length
/* QML list lacks any usable functions, so convert to an Array */
var itemList = new Array()
for (var i = 0; i < items.length; i++)
itemList.push(items[i])
- first = typeof first !== 'undefined' ? first : 0
- count = typeof count !== 'undefined' ? count : items.length
-
- var sumAspect = 0
- var rowHeight = 0
- var constantRowHeight = 0
- var rowStart = 0
- var y = padding
-
- for (var i = first; i < first+count; i++) {
- var c = itemList[i]
- if (!c.isLoaded)
- return
-
- /* Skip items with a width or height of 0 */
- if (!c.sourceSize.width || !c.sourceSize.height)
- continue
-
- sumAspect += c.sourceSize.width / c.sourceSize.height
- // i+2 is number of items + 1 for the right edge pad
- rowHeight = (width - padding * ((i-rowStart)+2)) / sumAspect
- if (rowHeight <= desiredHeight) {
- layoutRow(itemList.slice(rowStart, i+1), y, rowHeight)
-
- if (constantRowHeight > 0 && Math.abs(constantRowHeight - rowHeight) > 0.0001)
- constantRowHeight = -1
- else if (!constantRowHeight)
- constantRowHeight = rowHeight
-
- y += rowHeight + padding
- sumAspect = rowHeight = 0
- rowStart = i+1
- }
- }
-
- /* If all other rows have the same height, use that for the last row too.
- * This looks better when all images are the same size, and the last row
- * has fewer images on it. */
- if (constantRowHeight > 0)
- rowHeight = constantRowHeight
-
- if (first + count >= items.length && rowStart < items.length)
- layoutRow(itemList.slice(rowStart), y, Math.min(rowHeight, desiredHeight))
- }
-
- function layoutRow(items, y, height) {
- var x = padding
- for (var i = 0; i < items.length; i++) {
- var c = items[i]
- c.height = height
- c.width = height / (c.sourceSize.height / c.sourceSize.width)
- c.x = x
- c.y = y
- c.visible = true
-
- x += c.width + padding
- }
+ Layout.layout(layoutRoot, itemList.slice(first, first+count))
}
}
190 qml/cascade/folderlayout.js
View
@@ -0,0 +1,190 @@
+var padding = 8
+var desiredHeight = 300
+var minHeight = 175
+var headerYOffset = 0
+
+var rows = new Array()
+
+function LayoutRow(num) {
+ this.number = num
+ this.items = new Array()
+ this.height = 0
+ this.y = 0
+ this.sumAspect = 0
+ this.bigItemIndex = -1
+}
+
+LayoutRow.prototype.addItem = function(item) {
+ this.items.push(item)
+ this.sumAspect += (item.sourceSize.width / item.sourceSize.height)
+}
+
+LayoutRow.prototype.addBigItem = function(item) {
+ this.bigItemIndex = this.items.length
+ this.items.push(item)
+}
+
+LayoutRow.prototype.takeItem = function(index) {
+ var item = this.items[index]
+ if (index == this.bigItemIndex)
+ this.bigItemIndex = -1
+ else
+ this.sumAspect -= (item.sourceSize.width / item.sourceSize.height)
+ this.items.splice(index, 1)
+}
+
+LayoutRow.prototype.heightForWidth = function(width) {
+ var w = width
+ if (this.bigItemIndex >= 0) {
+ w -= this.items[this.bigItemIndex].width
+ if (w < 0)
+ return desiredHeight
+ }
+ return (w - padding * (this.items.length+1)) / this.sumAspect
+}
+
+LayoutRow.prototype.applyItemPositions = function() {
+ var x = padding
+
+ for (var i = 0; i < this.items.length; i++) {
+ var c = this.items[i]
+
+ if (i !== this.bigItemIndex) {
+ c.height = this.height
+ c.width = this.height / (c.sourceSize.height / c.sourceSize.width)
+ c.x = x
+ c.y = this.y
+ c.visible = true
+ }
+
+ x += c.width + padding
+ }
+}
+
+function layout(parent, items) {
+ if (!items.length)
+ return
+
+ var rowHeight = 0
+ var constantRowHeight = 0
+ var y = padding
+ var bigItem = null
+ var rowNumber = 0
+
+ var width = parent.width
+ var height = parent.height
+
+ var row = null
+ rows = new Array()
+
+ for (var i = 0; i < items.length; i++) {
+ var c = items[i]
+ if (!c.isLoaded)
+ return
+
+ /* Skip items with a width or height of 0 */
+ if (!c.sourceSize.width || !c.sourceSize.height)
+ continue
+
+ if (!row) {
+ row = new LayoutRow(rowNumber)
+ rows.push(row)
+ if (bigItem !== null)
+ row.addBigItem(bigItem)
+ }
+
+ if (!bigItem && !i) {
+ bigItem = c
+ /* Base the item's size to fill 55% of the area on its long edge, with
+ * the short edge keeping aspect ratio. Width is static, but height is
+ * subject to change to match rows. */
+ if (c.sourceSize.height > c.sourceSize.width) {
+ c.height = height * 0.55
+ c.width = c.height * (c.sourceSize.width / c.sourceSize.height)
+ } else {
+ c.width = width * 0.55
+ c.height = c.width * (c.sourceSize.height / c.sourceSize.width)
+ }
+ c.scaleMode = FastImage.Crop
+ c.x = padding
+ c.y = y
+ y += headerYOffset
+ row.addBigItem(c)
+ continue
+ } else
+ row.addItem(c)
+
+ rowHeight = row.heightForWidth(width)
+
+ /* XXX should we change these rules to actually consider what would be best for the
+ * big item, instead of treating it as an afterthought? */
+ if (rowHeight <= desiredHeight || rowHeight < minHeight) {
+ row.height = rowHeight
+ row.y = y
+
+ if (rowHeight < minHeight && row.items.length > 2) {
+ row.takeItem(row.items.length-1)
+ rowHeight = row.height = row.heightForWidth(width)
+ i--
+ }
+
+ if (bigItem) {
+ var bigItemRow = null
+ if (rowNumber && y + rowHeight >= height * 0.75) {
+ /* 75% height rule for the big item:
+ * If including the big item on this row would cause its height to exceed
+ * 75% of the total viewport height, do not include it on this row. This
+ * prevents certain cases where the big item will end up dominating too
+ * much of the view area, due to height expansion */
+ row.takeItem(row.bigItemIndex)
+ bigItemRow = rows[rowNumber-1]
+ } else if (row.height + row.y >= bigItem.height + bigItem.y) {
+ bigItemRow = row
+ }
+
+ if (bigItemRow) {
+ bigItem.height = bigItemRow.y + bigItemRow.height - bigItem.y
+ bigItem.visible = true
+ bigItem = null
+
+ /* Continue on the current row if we just removed the big item from it */
+ if (bigItemRow !== row)
+ continue
+ }
+ }
+
+ row.applyItemPositions()
+
+ if (constantRowHeight > 0 && Math.abs(constantRowHeight - rowHeight) > 0.0001)
+ constantRowHeight = -1
+ else if (!constantRowHeight)
+ constantRowHeight = rowHeight
+
+ y += rowHeight + padding
+ row = null
+ rowNumber++
+ }
+ }
+
+ /* If all other rows have the same height, use that for the last row too.
+ * This looks better when all images are the same size, and the last row
+ * has fewer images on it. */
+ if (constantRowHeight > 0)
+ rowHeight = constantRowHeight
+
+ if (parent.hasMoreFiles && y < height) {
+ parent.populate(parent.lastItemPos+1, 6)
+ } else if (row !== null) {
+ row.y = y
+ row.height = Math.min(rowHeight, desiredHeight)
+ row.applyItemPositions()
+ }
+
+ if (bigItem && !bigItem.visible) {
+ if (rows.length > 1)
+ bigItem.height = rows[1].y + rows[1].height - padding
+ else
+ bigItem.height = 2*desiredHeight + padding
+ bigItem.visible = true
+ }
+}
8 qml/cascade/main.qml
View
@@ -71,13 +71,13 @@ Rectangle {
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Left:
- cascade.currentFile = cascade.currentFile.parent
+ cascade.currentFile = cascade.leaveCurrentFolder()
break
case Qt.Key_Up:
cascade.currentFile = cascade.currentFile.parent.at(cascade.currentFile.index-1)
break
case Qt.Key_Right:
- cascade.currentFile = cascade.currentFile.at(0)
+ cascade.currentFile = cascade.enterCurrentFolder()
break
case Qt.Key_Down:
cascade.currentFile = cascade.currentFile.parent.at(cascade.currentFile.index+1)
@@ -119,9 +119,9 @@ Rectangle {
if (Math.abs(x) > Math.abs(y)) {
if (x > 10)
- cascade.currentFile = cascade.currentFile.at(0)
+ cascade.currentFile = cascade.enterCurrentFolder()
else if (x < -10)
- cascade.currentFile = cascade.currentFile.parent
+ cascade.currentFile = cascade.leaveCurrentFolder()
} else {
if (y > 10)
cascade.currentFile = cascade.currentFile.parent.at(cascade.currentFile.index+1)
10 qml/cascade/util.js
View
@@ -0,0 +1,10 @@
+.pragma library
+
+function pluralize(n, str, plural) {
+ plural = typeof plural !== 'undefined' ? plural : (str + "s")
+
+ if (n == 1)
+ return n + str
+ else
+ return n + plural
+}
2  qml/qml.qrc
View
@@ -5,5 +5,7 @@
<file>cascade/FilmstripArea.qml</file>
<file>cascade/FolderLayout.qml</file>
<file>cascade/main.qml</file>
+ <file>cascade/folderlayout.js</file>
+ <file>cascade/util.js</file>
</qresource>
</RCC>
23 src/cascade.cpp
View
@@ -13,7 +13,7 @@ Cascade *cascade = 0;
Cascade::Cascade(QObject *parent)
: QObject(parent), decoder(new ImageDecodingCache(this)), m_root(0), m_current(0),
- scanner(0), scanThread(0)
+ m_folderReturnItem(0), scanner(0), scanThread(0)
{
cascade = this;
@@ -87,6 +87,7 @@ void Cascade::setRoot(QString path)
m_root->setFiles(QList<File*>() << m_current);
} else
m_current = 0;
+ m_folderReturnItem = 0;
emit rootChanged();
emit currentFileChanged(m_current);
@@ -142,10 +143,30 @@ void Cascade::setCurrentFile(File *file)
{
if (m_current == file || !file || file == m_root)
return;
+
+ if (m_current && file == m_current->parent())
+ m_folderReturnItem = m_current;
+
m_current = file;
emit currentFileChanged(file);
}
+void Cascade::enterCurrentFolder()
+{
+ if (m_current && m_current->isDirectory()) {
+ if (m_folderReturnItem && m_current == m_folderReturnItem->parent())
+ setCurrentFile(m_folderReturnItem);
+ else
+ setCurrentFile(m_current->at(0));
+ }
+}
+
+void Cascade::leaveCurrentFolder()
+{
+ if (m_current)
+ setCurrentFile(m_current->parent());
+}
+
bool Cascade::eventFilter(QObject *, QEvent *event)
{
switch (event->type())
4 src/cascade.h
View
@@ -31,6 +31,8 @@ class Cascade : public QObject
public slots:
void setRoot(QString path);
void setCurrentFile(File *file);
+ void enterCurrentFolder();
+ void leaveCurrentFolder();
signals:
void rootChanged();
@@ -46,7 +48,7 @@ private slots:
private:
File *m_root;
- File *m_current;
+ File *m_current, *m_folderReturnItem;
FileSystemScanner *scanner;
QThread *scanThread;
QSet<QString> m_extensions;
36 src/file.cpp
View
@@ -47,6 +47,42 @@ QString File::path() const
return m_parent->path() + m_name;
}
+int File::fileCount() const
+{
+ int c = m_files.size();
+ foreach (File *f, m_files)
+ c -= f->isDirectory();
+ return c;
+}
+
+int File::totalFileCount() const
+{
+ int c = m_files.size();
+ foreach (File *f, m_files) {
+ if (f->isDirectory())
+ c += f->totalFileCount() - 1;
+ }
+ return c;
+}
+
+int File::folderCount() const
+{
+ int c = 0;
+ foreach (File *f, m_files)
+ c += f->isDirectory();
+ return c;
+}
+
+int File::totalFolderCount() const
+{
+ int c = 0;
+ foreach (File *f, m_files) {
+ if (f->isDirectory())
+ c += f->totalFolderCount() + 1;
+ }
+ return c;
+}
+
File *File::next() const
{
if (!m_parent)
9 src/file.h
View
@@ -15,7 +15,11 @@ class File : public QObject
Q_PROPERTY(int index READ index CONSTANT)
Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(bool isDirectory READ isDirectory CONSTANT)
+
Q_PROPERTY(int fileCount READ fileCount CONSTANT)
+ Q_PROPERTY(int totalFileCount READ totalFileCount CONSTANT)
+ Q_PROPERTY(int folderCount READ folderCount CONSTANT)
+ Q_PROPERTY(int totalFolderCount READ totalFolderCount CONSTANT)
Q_PROPERTY(File* next READ next)
Q_PROPERTY(File* nextIn READ nextIn)
@@ -36,7 +40,10 @@ class File : public QObject
bool isDirectory() const { return m_isDirectory; }
QList<File*> files() const { return m_files; }
Q_INVOKABLE void setFiles(QList<File*> files);
- int fileCount() const { return m_files.size(); }
+ int fileCount() const;
+ int totalFileCount() const;
+ int folderCount() const;
+ int totalFolderCount() const;
Q_INVOKABLE File *at(int index) const { return m_files.value(index); }

No commit comments for this range

Something went wrong with that request. Please try again.