Implement Properties
Implement a properties sidebar. In order to prevent the UI from freezing, we use futures to load and cancel loading file information. We also make use of `gdk_pixbuf` and `svg_metadata` in order to get the file's metadata before the file fully loads, meaning there's little to no delay between changing files and seeing the metadata for large images. The properties widget uses expressions and properties to avoid glue code for setting each label, allowing us to have a clean separation between UI and data.
This commit is contained in:
parent
1e95dd4cbb
commit
ba4661031f
12 changed files with 664 additions and 45 deletions
72
Cargo.lock
generated
72
Cargo.lock
generated
|
|
@ -232,6 +232,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easy-parallel"
|
name = "easy-parallel"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
|
|
@ -299,9 +305,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
|
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -314,9 +320,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
|
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
|
@ -324,15 +330,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
|
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
|
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
|
@ -341,9 +347,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
|
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
|
|
@ -362,9 +368,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
|
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -373,21 +379,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
|
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
|
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
|
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -863,14 +869,17 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
|
"futures",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"gettext-rs",
|
"gettext-rs",
|
||||||
|
"gio",
|
||||||
"gtk-macros",
|
"gtk-macros",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"libadwaita",
|
"libadwaita",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"svg_metadata",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1157,6 +1166,15 @@ version = "0.6.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671"
|
||||||
|
dependencies = [
|
||||||
|
"xmlparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -1249,6 +1267,18 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "svg_metadata"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d39f36262efe61096a17aa42140b0e44b9189806c3fa71ab3cff123429938eb0"
|
||||||
|
dependencies = [
|
||||||
|
"doc-comment",
|
||||||
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
|
"roxmltree",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.83"
|
version = "1.0.83"
|
||||||
|
|
@ -1399,6 +1429,12 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmlparser"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ log = "0.4.11"
|
||||||
once_cell = "1.9.0"
|
once_cell = "1.9.0"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
futures-channel = "0.3.19"
|
futures-channel = "0.3.19"
|
||||||
|
futures = "0.3.21"
|
||||||
|
svg_metadata = "0.4.2"
|
||||||
|
|
||||||
[dependencies.ashpd]
|
[dependencies.ashpd]
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -20,6 +22,11 @@ package = "gtk4"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
features = ["v4_6"]
|
features = ["v4_6"]
|
||||||
|
|
||||||
|
[dependencies.gio]
|
||||||
|
package = "gio"
|
||||||
|
version = "0.15.2"
|
||||||
|
features = ["v2_70"]
|
||||||
|
|
||||||
[dependencies.adw]
|
[dependencies.adw]
|
||||||
package = "libadwaita"
|
package = "libadwaita"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
110
data/gtk/properties_view.ui
Normal file
110
data/gtk/properties_view.ui
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="LpPropertiesView" parent="AdwBin">
|
||||||
|
<property name="width-request">255</property>
|
||||||
|
<property name="child">
|
||||||
|
<object class="AdwPreferencesPage">
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">Folder</property>
|
||||||
|
<property name="activatable-widget">folder_button</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<closure type="gchararray" function="folder_name">
|
||||||
|
<lookup name="file">LpPropertiesView</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<child type="suffix">
|
||||||
|
<object class="GtkButton" id="folder_button">
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="action-name">properties.open-folder</property>
|
||||||
|
<property name="icon-name">folder-open-symbolic</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Open Containing Folder</property>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">Image Size</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<lookup name="dimensions">LpPropertiesView</lookup>
|
||||||
|
</binding>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">File Type</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<closure type="gchararray" function="file_type">
|
||||||
|
<lookup name="file-info">LpPropertiesView</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">File Size</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<closure type="gchararray" function="file_size">
|
||||||
|
<lookup name="file-info">LpPropertiesView</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">Created</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<closure type="gchararray" function="created_date">
|
||||||
|
<lookup name="file-info">LpPropertiesView</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow">
|
||||||
|
<property name="title" translatable="yes">Modified</property>
|
||||||
|
<binding name="subtitle">
|
||||||
|
<closure type="gchararray" function="modified_date">
|
||||||
|
<lookup name="file-info">LpPropertiesView</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<style>
|
||||||
|
<class name="property"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</template>
|
||||||
|
</interface>
|
||||||
|
|
@ -18,6 +18,12 @@
|
||||||
<property name="swipe-to-close">False</property>
|
<property name="swipe-to-close">False</property>
|
||||||
<property name="locked">true</property>
|
<property name="locked">true</property>
|
||||||
<property name="modal">false</property>
|
<property name="modal">false</property>
|
||||||
|
<binding name="fold-policy">
|
||||||
|
<closure type="AdwFlapFoldPolicy" function="fold_policy">
|
||||||
|
<lookup name="fullscreened">LpWindow</lookup>
|
||||||
|
<lookup type="AdwFlap" name="reveal-flap">properties_flap</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
<property name="flap">
|
<property name="flap">
|
||||||
<object class="GtkHeaderBar" id="headerbar">
|
<object class="GtkHeaderBar" id="headerbar">
|
||||||
<property name="show_title_buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean"/>
|
<property name="show_title_buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean"/>
|
||||||
|
|
@ -35,39 +41,60 @@
|
||||||
<property name="tooltip-text" translatable="yes">Primary Menu</property>
|
<property name="tooltip-text" translatable="yes">Primary Menu</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child type="end">
|
||||||
|
<object class="GtkToggleButton" id="properties_button">
|
||||||
|
<property name="icon-name">help-about-symbolic</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
<property name="content">
|
<property name="content">
|
||||||
<object class="AdwToastOverlay" id="toast_overlay">
|
<object class="AdwToastOverlay" id="toast_overlay">
|
||||||
<property name="child">
|
<property name="child">
|
||||||
<object class="GtkStack" id="stack">
|
<object class="AdwFlap" id="properties_flap">
|
||||||
<property name="visible-child">status_page</property>
|
<property name="flap-position">end</property>
|
||||||
<property name="vexpand">True</property>
|
<property name="fold-policy">never</property>
|
||||||
<child>
|
<property name="locked">True</property>
|
||||||
<object class="AdwStatusPage" id="status_page">
|
<property name="reveal-flap" bind-source="properties_button" bind-property="active" bind-flags="bidirectional|sync-create"/>
|
||||||
<property name="title" translatable="yes">Image Viewer</property>
|
<property name="content">
|
||||||
<property name="description" translatable="yes">Click "Open File…" or drag an image here.</property>
|
<object class="GtkStack" id="stack">
|
||||||
|
<property name="visible-child">status_page</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton">
|
<object class="AdwStatusPage" id="status_page">
|
||||||
<property name="halign">center</property>
|
<property name="title" translatable="yes">Image Viewer</property>
|
||||||
<property name="label" translatable="yes">Open File…</property>
|
<property name="description" translatable="yes">Click "Open File…" or drag an image here.</property>
|
||||||
<property name="action-name">win.open</property>
|
<child>
|
||||||
<style>
|
<object class="GtkButton">
|
||||||
<class name="pill"/>
|
<property name="halign">center</property>
|
||||||
<class name="suggested-action"/>
|
<property name="label" translatable="yes">Open File…</property>
|
||||||
</style>
|
<property name="action-name">win.open</property>
|
||||||
|
<style>
|
||||||
|
<class name="pill"/>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="LpImageView" id="image_view"/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkDropTarget" id="drop_target">
|
||||||
|
<property name="actions">copy</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</property>
|
||||||
<child>
|
<property name="separator">
|
||||||
<object class="LpImageView" id="image_view"/>
|
<object class="GtkSeparator"/>
|
||||||
</child>
|
</property>
|
||||||
<child>
|
<property name="flap">
|
||||||
<object class="GtkDropTarget" id="drop_target">
|
<object class="LpPropertiesView" id="properties_view">
|
||||||
<property name="actions">copy</property>
|
<property name="file" bind-source="image_view" bind-property="active-file" bind-flags="sync-create"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/window.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/window.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/image_page.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/image_page.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks">gtk/image_view.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks">gtk/image_view.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks">gtk/properties_view.ui</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
|
<file>style-hc.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|
|
||||||
3
data/style-hc.css
Normal file
3
data/style-hc.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
row.property label.title {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
@ -36,4 +36,14 @@ flap.fullscreen-widget dimming, shadow, border, outline {
|
||||||
);
|
);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.property label.title {
|
||||||
|
opacity: 0.55;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.property label.subtitle {
|
||||||
|
opacity: 1.0;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ rust_sources = files(
|
||||||
'widgets/image_page.rs',
|
'widgets/image_page.rs',
|
||||||
'widgets/image_view.rs',
|
'widgets/image_view.rs',
|
||||||
'widgets/mod.rs',
|
'widgets/mod.rs',
|
||||||
|
'widgets/properties_view.rs',
|
||||||
'application.rs',
|
'application.rs',
|
||||||
'config.rs',
|
'config.rs',
|
||||||
'file_model.rs',
|
'file_model.rs',
|
||||||
|
|
|
||||||
19
src/util.rs
19
src/util.rs
|
|
@ -37,6 +37,25 @@ pub fn query_attributes(file: &gio::File, attributes: Vec<&str>) -> anyhow::Resu
|
||||||
.context("Failed to query string")
|
.context("Failed to query string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn query_attributes_future(
|
||||||
|
file: &gio::File,
|
||||||
|
attributes: Vec<&str>,
|
||||||
|
) -> anyhow::Result<gio::FileInfo> {
|
||||||
|
let mut attr_str = String::from(*attributes.get(0).context("No attributes")?);
|
||||||
|
|
||||||
|
for attr in &attributes[1..] {
|
||||||
|
write!(attr_str, ",{}", attr)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.query_info_future(
|
||||||
|
&attr_str,
|
||||||
|
gio::FileQueryInfoFlags::empty(),
|
||||||
|
glib::Priority::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to query attributes")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn compare_by_name(file_a: &gio::File, file_b: &gio::File) -> std::cmp::Ordering {
|
pub fn compare_by_name(file_a: &gio::File, file_b: &gio::File) -> std::cmp::Ordering {
|
||||||
let name_a = get_file_display_name(file_a).unwrap_or_default();
|
let name_a = get_file_display_name(file_a).unwrap_or_default();
|
||||||
let name_b = get_file_display_name(file_b).unwrap_or_default();
|
let name_b = get_file_display_name(file_b).unwrap_or_default();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@
|
||||||
mod image;
|
mod image;
|
||||||
mod image_page;
|
mod image_page;
|
||||||
mod image_view;
|
mod image_view;
|
||||||
|
mod properties_view;
|
||||||
|
|
||||||
pub use image::LpImage;
|
pub use image::LpImage;
|
||||||
pub use image_page::LpImagePage;
|
pub use image_page::LpImagePage;
|
||||||
pub use image_view::LpImageView;
|
pub use image_view::LpImageView;
|
||||||
|
pub use properties_view::LpPropertiesView;
|
||||||
|
|
|
||||||
388
src/widgets/properties_view.rs
Normal file
388
src/widgets/properties_view.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
// properties_view.rs
|
||||||
|
//
|
||||||
|
// Copyright $year Christopher Davis <christopherdavis@gnome.org>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use crate::deps::*;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use adw::prelude::*;
|
||||||
|
use adw::subclass::prelude::*;
|
||||||
|
use glib::clone;
|
||||||
|
use gtk::subclass::prelude::*;
|
||||||
|
use gtk::CompositeTemplate;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use ashpd::desktop::open_uri;
|
||||||
|
use ashpd::WindowIdentifier;
|
||||||
|
use futures::future::{AbortHandle, Abortable};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
const MB: f64 = 1_000_000.0;
|
||||||
|
const KB: f64 = 1_000.0;
|
||||||
|
const FALLBACK: &str = "-";
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, CompositeTemplate)]
|
||||||
|
#[template(resource = "/org/gnome/Loupe/gtk/properties_view.ui")]
|
||||||
|
pub struct LpPropertiesView {
|
||||||
|
pub file: RefCell<Option<glib::WeakRef<gio::File>>>,
|
||||||
|
pub file_info: RefCell<Option<gio::FileInfo>>,
|
||||||
|
pub dimensions: RefCell<Option<String>>,
|
||||||
|
|
||||||
|
pub info_handle: RefCell<Option<AbortHandle>>,
|
||||||
|
pub dimensions_handle: RefCell<Option<AbortHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for LpPropertiesView {
|
||||||
|
const NAME: &'static str = "LpPropertiesView";
|
||||||
|
type Type = super::LpPropertiesView;
|
||||||
|
type ParentType = adw::Bin;
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
Self::bind_template(klass);
|
||||||
|
Self::Type::bind_template_callbacks(klass);
|
||||||
|
|
||||||
|
klass.install_action("properties.open-folder", None, move |properties, _, _| {
|
||||||
|
properties.open_directory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||||
|
obj.init_template();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for LpPropertiesView {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpecObject::new(
|
||||||
|
"file",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
gio::File::static_type(),
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
),
|
||||||
|
glib::ParamSpecObject::new(
|
||||||
|
"file-info",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
gio::FileInfo::static_type(),
|
||||||
|
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
),
|
||||||
|
glib::ParamSpecString::new(
|
||||||
|
"dimensions",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
Some(FALLBACK),
|
||||||
|
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
let prop_name = pspec.name();
|
||||||
|
|
||||||
|
match prop_name {
|
||||||
|
"file" => obj.file().to_value(),
|
||||||
|
"file-info" => obj.file_info().to_value(),
|
||||||
|
"dimensions" => self.dimensions.borrow().to_value(),
|
||||||
|
_ => unimplemented!("Failed to get property \"{prop_name}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
let prop_name = pspec.name();
|
||||||
|
|
||||||
|
match prop_name {
|
||||||
|
"file" => obj.set_file(&value.get().ok()),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
obj.action_set_enabled("properties.open-folder", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for LpPropertiesView {}
|
||||||
|
impl BinImpl for LpPropertiesView {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct LpPropertiesView(ObjectSubclass<imp::LpPropertiesView>)
|
||||||
|
@extends gtk::Widget, adw::Bin,
|
||||||
|
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl LpPropertiesView {
|
||||||
|
fn set_file(&self, file: &Option<gio::File>) {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
|
self.action_set_enabled("properties.open-folder", file.is_some());
|
||||||
|
|
||||||
|
if let Some(file) = file {
|
||||||
|
if let Some(current_file) = self.file() {
|
||||||
|
if current_file.equal(file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the file info future. See `build_file_info()` for details.
|
||||||
|
if let Some(handle) = imp.info_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(handle) = imp.dimensions_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.build_file_info(file);
|
||||||
|
let weak = file.downgrade();
|
||||||
|
imp.file.replace(Some(weak));
|
||||||
|
self.notify("file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file(&self) -> Option<gio::File> {
|
||||||
|
let imp = self.imp();
|
||||||
|
imp.file.borrow().as_ref().and_then(|w| w.upgrade())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_info(&self) -> Option<gio::FileInfo> {
|
||||||
|
let imp = self.imp();
|
||||||
|
imp.file_info.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_file_info(&self, file: &gio::File) {
|
||||||
|
let imp = self.imp();
|
||||||
|
let ctx = glib::MainContext::default();
|
||||||
|
|
||||||
|
// We need to be able to cancel this future so that
|
||||||
|
// changing files before the metadata loads does not cause
|
||||||
|
// old metadata to be loaded. In order to make a cancellable
|
||||||
|
// future, we use `AbortHandle` and `Abortable` from the `futures`
|
||||||
|
// crate.
|
||||||
|
//
|
||||||
|
// `handle` is the `AbortHandle`, which we need to store in order
|
||||||
|
// to abort the future. `reg` is the `AbortRegistration` that we
|
||||||
|
// give to `Abortable::new()` to tie our future to the `AbortHandle`.
|
||||||
|
//
|
||||||
|
// Here we build the future:
|
||||||
|
let (handle, reg) = AbortHandle::new_pair();
|
||||||
|
let fut = Abortable::new(
|
||||||
|
clone!(@weak self as view, @weak file => async move {
|
||||||
|
if let Ok(info) = util::query_attributes_future(
|
||||||
|
&file,
|
||||||
|
vec![
|
||||||
|
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
||||||
|
&gio::FILE_ATTRIBUTE_STANDARD_SIZE,
|
||||||
|
&gio::FILE_ATTRIBUTE_TIME_CREATED,
|
||||||
|
&gio::FILE_ATTRIBUTE_TIME_MODIFIED,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await {
|
||||||
|
view.on_info_loaded(info, &file);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
reg,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ...store the handle:
|
||||||
|
imp.info_handle.replace(Some(handle));
|
||||||
|
|
||||||
|
// ...then spawn the future.
|
||||||
|
ctx.spawn_local(async {
|
||||||
|
let _ = fut.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is where we handle the results of the future.
|
||||||
|
// This could technically be part of the future, but we keep
|
||||||
|
// them separate for a bit of cleanliness.
|
||||||
|
fn on_info_loaded(&self, info: gio::FileInfo, file: &gio::File) {
|
||||||
|
let imp = self.imp();
|
||||||
|
self.build_dimensions(&info, file);
|
||||||
|
imp.file_info.replace(Some(info));
|
||||||
|
self.notify("file-info");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dimensions(&self, info: &gio::FileInfo, file: &gio::File) {
|
||||||
|
let imp = self.imp();
|
||||||
|
let ctx = glib::MainContext::default();
|
||||||
|
|
||||||
|
// Here we do the same thing we do for the rest of the file info.
|
||||||
|
let (handle, reg) = AbortHandle::new_pair();
|
||||||
|
let fut = Abortable::new(
|
||||||
|
clone!(@weak self as view, @weak file, @weak info => async move {
|
||||||
|
let is_svg = info.content_type()
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.map(|t| t == "image/svg+xml")
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let path = file.peek_path();
|
||||||
|
if path.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimensions = if is_svg {
|
||||||
|
parse_svg_file_future(path.unwrap())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| {
|
||||||
|
match (m.width(), m.height()) {
|
||||||
|
(Some(w), Some(h)) => Some((w, h)),
|
||||||
|
_ => None::<(f64, f64)>,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|(w, h)| format!("{w:.1} \u{D7} {h:.1}"))
|
||||||
|
} else {
|
||||||
|
gdk_pixbuf::Pixbuf::file_info_future(path.unwrap())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|(_, w, h)| format!("{w} \u{D7} {h}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
view.on_dimensions_loaded(dimensions.unwrap_or_else(|| FALLBACK.to_string()));
|
||||||
|
}),
|
||||||
|
reg,
|
||||||
|
);
|
||||||
|
|
||||||
|
imp.dimensions_handle.replace(Some(handle));
|
||||||
|
|
||||||
|
ctx.spawn_local(async {
|
||||||
|
let _ = fut.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_dimensions_loaded(&self, dimensions: String) {
|
||||||
|
let imp = self.imp();
|
||||||
|
imp.dimensions.replace(Some(dimensions));
|
||||||
|
self.notify("dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_directory(&self) {
|
||||||
|
if let Some(directory) = self
|
||||||
|
.file()
|
||||||
|
.and_then(|f| f.peek_path())
|
||||||
|
.and_then(|p| std::fs::File::open(p).ok())
|
||||||
|
{
|
||||||
|
let ctx = glib::MainContext::default();
|
||||||
|
|
||||||
|
ctx.spawn_local(clone!(@weak self as view => async move {
|
||||||
|
let id = WindowIdentifier::from_native(&view.native().expect("No GtkNative for view")).await;
|
||||||
|
if let Err(e) = open_uri::open_directory(&id, &directory).await {
|
||||||
|
log::error!("Could not open parent directory: {e}");
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the LpPropertiesView UI file we define a few `gtk::Expression`s
|
||||||
|
// that are closures. These closures take either the current `gio::File`
|
||||||
|
// or the current file's associated `gio::FileInfo` and process them
|
||||||
|
// accordingly.
|
||||||
|
//
|
||||||
|
// In this function we chain `Option`s with `and_then()` in order
|
||||||
|
// to handle optional results with a fallback, without needing to
|
||||||
|
// have multiple `match` or `if let` branches, and without needing
|
||||||
|
// to unwrap.
|
||||||
|
#[template_callback]
|
||||||
|
fn folder_name(&self, file: Option<gio::File>) -> String {
|
||||||
|
file.and_then(|f| f.parent()) // If the file exists, get the parent
|
||||||
|
.and_then(|p| util::get_file_display_name(&p)) // if that exists, the display name
|
||||||
|
.unwrap_or_else(|| FALLBACK.to_owned()) // and if we get nothing, use `FALLBACK`
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn file_type(&self, info: Option<gio::FileInfo>) -> String {
|
||||||
|
info.and_then(|i| i.content_type())
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.map(|t| {
|
||||||
|
t.rsplit('/').collect::<Vec<&str>>()[0]
|
||||||
|
.to_owned()
|
||||||
|
.to_uppercase()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| FALLBACK.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn file_size(&self, info: Option<gio::FileInfo>) -> String {
|
||||||
|
info.map(|info| {
|
||||||
|
let bytes = info.size() as f64;
|
||||||
|
|
||||||
|
let (mut size, mut unit) = (bytes, "B");
|
||||||
|
let sizes = [(MB, "MB"), (KB, "kB")];
|
||||||
|
|
||||||
|
for (b, u) in sizes {
|
||||||
|
if bytes > b {
|
||||||
|
let res = bytes / b;
|
||||||
|
size = res;
|
||||||
|
unit = u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String::from(&format!("{size:.1} {unit}"))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| FALLBACK.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn created_date(&self, info: Option<gio::FileInfo>) -> String {
|
||||||
|
info.and_then(|i| i.creation_date_time())
|
||||||
|
.and_then(|t| t.format("%x, %X").ok())
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.unwrap_or_else(|| FALLBACK.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn modified_date(&self, info: Option<gio::FileInfo>) -> String {
|
||||||
|
info.and_then(|i| i.modification_date_time())
|
||||||
|
.and_then(|t| t.format("%x, %X").ok())
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.unwrap_or_else(|| FALLBACK.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_svg_file_future(path: std::path::PathBuf) -> anyhow::Result<svg_metadata::Metadata> {
|
||||||
|
let (sender, receiver) = futures_channel::oneshot::channel();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = svg_metadata::Metadata::parse_file(path);
|
||||||
|
sender.send(result).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
receiver.await.unwrap().context("Could not load metadata")
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ use ashpd::WindowIdentifier;
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::widgets::LpImageView;
|
use crate::widgets::{LpImageView, LpPropertiesView};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -70,6 +70,8 @@ mod imp {
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub image_view: TemplateChild<LpImageView>,
|
pub image_view: TemplateChild<LpImageView>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
|
pub properties_view: TemplateChild<LpPropertiesView>,
|
||||||
|
#[template_child]
|
||||||
pub drop_target: TemplateChild<gtk::DropTarget>,
|
pub drop_target: TemplateChild<gtk::DropTarget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,11 +211,11 @@ impl LpWindow {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
|
||||||
if fullscreen {
|
if fullscreen {
|
||||||
imp.flap.set_fold_policy(adw::FlapFoldPolicy::Always);
|
|
||||||
imp.headerbar.add_css_class("osd");
|
imp.headerbar.add_css_class("osd");
|
||||||
|
imp.properties_view.add_css_class("osd")
|
||||||
} else {
|
} else {
|
||||||
imp.flap.set_fold_policy(adw::FlapFoldPolicy::Never);
|
|
||||||
imp.headerbar.remove_css_class("osd");
|
imp.headerbar.remove_css_class("osd");
|
||||||
|
imp.properties_view.remove_css_class("osd")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_fullscreened(fullscreen);
|
self.set_fullscreened(fullscreen);
|
||||||
|
|
@ -382,4 +384,16 @@ impl LpWindow {
|
||||||
file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
|
file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
|
||||||
.unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing
|
.unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We also have a closure that returns `adw::FlapFoldPolicy`.
|
||||||
|
// if we aren't fullscreened, or if the properties are revealed,
|
||||||
|
// we unfold the main flap. Otherwise we're always folded.
|
||||||
|
#[template_callback]
|
||||||
|
fn fold_policy(&self, fullscreened: bool, properties_revealed: bool) -> adw::FlapFoldPolicy {
|
||||||
|
if !fullscreened || properties_revealed {
|
||||||
|
adw::FlapFoldPolicy::Never
|
||||||
|
} else {
|
||||||
|
adw::FlapFoldPolicy::Always
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue