Skip to content
Commits on Source (91)
......@@ -3,20 +3,34 @@ SPDX-License-Identifier: CC-BY-SA-4.0
SPDX-FileCopyrightText: Michael Terry
-->
# Welcome
This guide is aimed at developers looking to build Déjà Dup locally and maybe contribute back a patch.
Thank you so much for helping out! That's so nice.
If you encounter a problems or just want to run an idea by us first before spending much time on it, please get in touch in our [chat room][chat].
[chat]: https://matrix.to/#/#deja-dup:gnome.org
# Building from a Source Release
This is recommended if you are a downstream packager of stable releases.
Though if you *are* a downstream packager, please read
[`PACKAGING.md`](PACKAGING.md) instead, as it has more relevant tips.
If you have downloaded this source from a tarball release (that is, a file like `.tar.bz2` or `.zip`),
you can use standard meson commands like:
* `meson --buildtype=release my-build-directory`
* `meson compile -C my-build-directory`
See the [meson documentation](https://mesonbuild.com/) for more guidance. And look at `meson_options.txt` for all the extra build options you can set.
See the [meson documentation](https://mesonbuild.com/) for more guidance.
And look at [`meson_options.txt`](meson_options.txt) for all the extra build options you can set.
# Building from a Git Clone
This is recommended if you intend to contribute back a patch. Git checkouts include a `Makefile` that make setting up a sandboxed development environment easier.
This is recommended if you intend to contribute back a patch.
Git checkouts include a [`Makefile`](Makefile) that make setting up a sandboxed development environment easier.
## Set Up the GNOME SDK
......@@ -26,7 +40,7 @@ To make sure you can build against the latest GNOME libraries, it helps to insta
1. `make devenv-setup` (this will install the GNOME SDK flatpaks and also build & install our own devel flatpak locally)
1. `make devenv`
Now you're inside a flatpak container (org.gnome.DejaDupDevel) with all dependencies installed.
Now you're inside a flatpak container (`org.gnome.DejaDupDevel`) with all dependencies installed.
From here, you can build and run `deja-dup` like so: `make && deja-dup`.
## Building
......@@ -44,12 +58,14 @@ From here, you can build and run `deja-dup` like so: `make && deja-dup`.
From inside a devenv shell, you can iterate as you develop by just running `deja-dup` directly.
But if you want to actually run the test suite, that's easy too:
* Running all unit tests: `meson test -C _build`
* Running one unit test: `meson test script-threshold-inc -C _build -v`
* Running one unit test: `meson test -C _build -v script-threshold-inc`
# Copyright
If you are making a [substantial patch](https://www.gnu.org/prep/maintain/html_node/Legally-Significant.html) (adding ~15 lines or more), add yourself to the top of the file in a new copyright line.
If you are making a [substantial patch](https://www.gnu.org/prep/maintain/html_node/Legally-Significant.html) (adding ~15 lines or more), add yourself to the top of the changed file in a new copyright line.
# Project Assets
......
......@@ -3,6 +3,32 @@ SPDX-License-Identifier: CC-BY-SA-4.0
SPDX-FileCopyrightText: Michael Terry
-->
# 44.0
- Refresh the visuals in a few places by using modern text entries and the
new About dialog
- Newly created restic backups will now use compression
- Fix a bug that prevented updating the folder option in the Preferences
window after changing to an external disk
- Fix a bug that prevented switching to the restore view if the app starts
up in mobile mode (thin width)
- Update Basque, Brazilian Portuguese, Catalan, Chinese (China), Croatian,
Danish, Dutch, Finnish, French, German, Hebrew, Hungarian, Indonesian,
Korean, Polish, Portuguese, Russian, Serbian, Slovenian, Swedish, Turkish,
and Ukrainian translations
##### Packaging
- There is now some documentation about how to package deja-dup in
[PACKAGING.md](PACKAGING.md) - walking through required & optional
dependencies and build options
- Add new `-Dpackagekit=enabled` option flag to control whether we build with
PackageKit support. Previously, this was an `auto` feature without an option
flag to control it, but is now `disabled` by default and you can explicitly
enable it if your packaging cannot directly depend on runtime dependencies
like duplicity. If you do enable this, read the above doc for a list of other
`pkgs` options to set as well.
- Require libadwaita1 1.2+
- If you enable restic, we now require restic 0.14+
# 43.4
- Warn about delayed backups due to power saver mode, if it's been over a day
since we were supposed to back up
......
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
SPDX-FileCopyrightText: Michael Terry
-->
# Packaging Déjà Dup
If you help package Déjà Dup for a distribution, let me first say:
Thank you so much!
This guide is designed to answer your most pressing concerns.
But if you have any further questions, ask away in our [chat room][chat].
If you are merely building Déjà Dup for yourself or hacking on it to make a
patch, try reading [`CONTRIBUTING.md`](CONTRIBUTING.md) instead.
[chat]: https://matrix.to/#/#deja-dup:gnome.org
## Required Dependencies
### Build-time Libraries
You can also see these listed in [`meson.build`](meson.build), but for your
convenience:
- Adwaita 1.2
- GLib 2.70
- GTK 4.6
- JSON-GLib 1.2
- libgpg-error 1.33
- libsecret 0.18.6
- libsoup 3.0
### Duplicity
Duplicity 0.7.14 or greater is a required runtime dependency.
Duplicity itself has a wide variety of possible Python dependencies based on
its many supported backends, but we only use and require a few of them.
You'll want to depend on the following runtime dependencies for your distro:
- Duplicity itself
- GVFS and its backends (it'd be nice to ensure the `afp`, `dav`, `ftp`, `nfs`,
`sftp`, and `smb` backends at least are installed)
- The Python module `gi` along with the typelibs for `Gio` and `GLib`
(all of which are usually provided by the `pygobject` project)
- The Python module `pydrive2`
- The Python module `requests_oauthlib`
## Optional Dependencies
### Restic
Restic is an alternative backup tool (instead of Duplicity) for which Déjà Dup
has experimental support. It is not enabled by default.
Eventually, we may use it for some advanced features that Duplicity can't
provide, like changing the encryption password or more fine-tuned storage
management.
Once enabled, users still have to opt-into the feature, with a big warning
about its experimental nature.
Thus, it should be safe to enable.
I'd love it if you could, so that I get more user testing and feedback.
Here's how you enable it as an opt-in feature for users:
- Set `-Denable_restic=true` when building
- Depend on `restic` 0.14.0
- Depend on `rclone`
### PackageKit
Some runtime dependencies may be difficult for you to directly depend on.
As an example, let's say that you're packaging Déjà Dup for Ubuntu and some
dependencies are in `universe`, but `deja-dup` itself is in `main` (which is
the actual historical impetus for this feature). You couldn't just directly
depend on those `universe` dependencies from a `main` package.
However, if you enable PackageKit support, Déjà Dup can prompt the user to
install missing dependencies on the fly, relatively seamlessly as part of the
backup or restore process.
This is disabled by default because it is not the preferred user experience.
Users may reasonably get weirded out by apps that are "missing" dependencies or
that ask for permission to install "arbitrary" packages.
That's not a typical thing for a user to see and might make them distrust their
backup software (which is normally supposed to be a secure and trustworthy
guardian of their data).
But if you need this, it's easy to enable:
- Add `libpackagekit-glib2` as a build dependency
- Set `-Dpackagekit=enabled` when building
- Set any or all of the following option flags. If you need multiple packages
for a single option, separate their names with commas.
- `duplicity_pkgs`: defaults to `duplicity`
- `gvfs_pkgs`: specify packages for GVFS as well as the `gi` Python module,
**no default**
- `pydrive_pkgs`: the `pydrive2` Python module, **no default**
- `rclone_pkgs`: defaults to `rclone`
- `requests_oauthlib_pkgs`: the `requests_oauthlib` Python module,
**no default**
- `restic_pkgs`: defaults to `restic`
An example for a Debian-style distro:
```
-Dpackagekit=enabled
-Dgvfs_pkgs=gvfs-backends,python3-gi
-Dpydrive_pkgs=python3-pydrive2
-Drequests_oauthlib_pkgs=python3-requests-oauthlib
```
## Dependencies Outside of PATH
If your distro installs packages in non-standard locations or with non-standard
names, you can still tell Déjà Dup where to find them.
For example, NixOS does not even have a global `/usr/bin`, instead installing
packages in namespaced directories for parallel installation.
Or maybe the Duplicity command gets installed as `duplicity.bin` on your distro.
I don't judge.
That's easy to tell us about. Just set the following option flags:
- `duplicity_command`
- `rclone_command`
- `restic_command`
Absolute paths will be used directly, while bare command names will be searched
for in `PATH`.
An example:
```
-Dduplicity_command=/opt/duplicity/bin/duplicity
```
## Monitoring for New Releases
All releases and tarballs can be found as [tags][tags] on our source repository
in GitLab.
If any packaging changes happen, they will always be mentioned in the release
notes (on the GitLab tag and in [`NEWS.md`](NEWS.md)).
Version numbers follow [GNOME style][versions] (but not their release schedule).
So a notable new release (like a redesigned UI or new dependencies) will get a
bumped major version number.
For example, we went from 43 to 44 because we bumped Adwaita's minimum version
and changed how a build option worked.
Development releases will look like 44.alpha and 44.beta (though not every
major release will bother with a full alpha/beta test cycle).
[tags]: https://gitlab.gnome.org/World/deja-dup/tags
[versions]: https://discourse.gnome.org/t/new-gnome-versioning-scheme/4235
......@@ -19,6 +19,13 @@ Déjà Dup focuses on ease of use and personal, accidental data loss.
If you need a full system backup or an archival program, you may prefer other
backup apps.
## Building
If you are hacking on Déjà Dup, see [CONTRIBUTING.md](CONTRIBUTING.md).
Or if you are packaging Déjà Dup for a distribution, see
[PACKAGING.md](PACKAGING.md) for extra tips.
## Links
* [Homepage](https://wiki.gnome.org/Apps/DejaDup)
......
......@@ -166,10 +166,10 @@ class BaseTest(unittest.TestCase):
window.child(roleName="text", label="Encryption password").text = password
window.child(roleName="text", label="Confirm password").text = password
if remember:
window.child(roleName="check box", name="_Remember password").click()
window.child(roleName="check box", name="Remember password").click()
else:
window.child(
roleName="check box", name="_Allow restoring without a password"
roleName="check box", name="Password-protect your backup"
).click()
window.button("Forward").click()
......@@ -212,7 +212,11 @@ class BaseTest(unittest.TestCase):
app.button("Continue").click()
def get_file_chooser(self, name):
return Gtk4Node(tree.root).child(roleName="dialog", name=name)
if os.environ["DD_MODE"] == "snap":
# snap uses native dialog with an old gtk
return Gtk4Node(tree.root).child(roleName="dialog", name=name)
else:
return Gtk4Node(tree.root).child(roleName="filler", name=name)
def click_restore_button(self, parent):
bar = parent.child(name="GtkActionBar")
......
......@@ -80,12 +80,10 @@ class Gtk4Node:
if not top:
top = self.findAncestor(GenericPredicate(roleName="dialog"))
if not top:
top = self.findAncestor(GenericPredicate(roleName="filler", name="Preferences"))
if not top:
# check for a gtk3 portal dialog - it won't need translation
if self.findAncestor(GenericPredicate(roleName="file chooser")):
self.coords = (0, 0)
return
maybe_top = self.findAncestor(GenericPredicate(roleName="filler"))
while maybe_top:
top = maybe_top
maybe_top = top.findAncestor(GenericPredicate(roleName="filler"))
assert top
from Xlib import X, display, Xutil
......
......@@ -4,6 +4,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Michael Terry
# Note that you need to be running under X11 for these tests to work
EXISTING=$(ps x -o pid,cmd | grep deja-dup | sed 's/^ *\([0-9]\+\).*/\1/')
if [ -n "$EXISTING" ]; then
kill -9 $EXISTING 2>/dev/null
......@@ -81,4 +83,4 @@ fi
# Dogtail requires this
gsettings set org.gnome.desktop.interface toolkit-accessibility true
exec pytest-3 $*
exec python3 -m pytest $*
......@@ -210,7 +210,7 @@ class BackupTest(BaseTest):
# Wait for prompt (a little longer to appear than normal dogtail timeouts)
self.wait_for(
lambda: app.findChild(
lambda x: x.roleName == "password text"
lambda x: x.roleName == "text"
and x.name == "Encryption password",
requireResult=False,
retry=False,
......
......@@ -126,9 +126,9 @@ class BrowserTest(BaseTest):
def select_location(self, where):
self.addCleanup(shutil.rmtree, where, ignore_errors=True)
self.window.child(
roleName="check box", name="Restore to _specific folder"
roleName="list item", name="Restore to specific folder"
).click()
self.window.child(roleName="push button", label="Choose Folder").click()
self.window.child(roleName="push button", name="Choose Folder").click()
os.makedirs(where, exist_ok=True)
dlg = self.get_file_chooser("Choose Folder")
# Focus dialog (not always done automatically with portal dialogs)
......
......@@ -4,6 +4,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Michael Terry
import os
from dogtail.predicate import GenericPredicate
from dogtail.rawinput import keyCombo
from gi.repository import GLib
......@@ -22,6 +24,13 @@ class PreferencesTest(BaseTest):
box = root.child(roleName="list item", name="Back Up Automatically")
return box.child(roleName="check box")
def get_preferences_window(self):
if os.environ["DD_MODE"] == "snap":
# Older gtk in snap seems to use frame not filler
return self.app.child(roleName="frame", name="Preferences")
else:
return self.app.child(roleName="filler", name="Preferences")
def test_general(self):
# Test that there's a special first time welcome screen
self.app.childNamed("Create Your First Backup")
......@@ -32,7 +41,7 @@ class PreferencesTest(BaseTest):
main = self.app.window("Backups")
periodic_main = self.get_auto_check(main)
prefs = self.app.child(roleName="filler", name="Preferences")
prefs = self.get_preferences_window()
# Periodic to settings
periodic = self.get_auto_check(prefs)
......@@ -92,11 +101,10 @@ class PreferencesTest(BaseTest):
self.wait_for(lambda: self.table_names(table) == names)
def assert_inclusion_table(self, widget, key):
prefs = self.app.child(roleName="filler", name="Preferences")
prefs = self.get_preferences_window()
prefs.child(name="Folders").click()
panel = prefs.child(roleName="panel", name=widget)
table = panel.child(roleName="list")
table = prefs.child(roleName="list", name=widget)
user = GLib.get_user_name()
home = GLib.get_home_dir()
......
......@@ -39,7 +39,10 @@ public class AssistantBackup : AssistantOperation
scroll.child = new ConfigFolderPage();
append_page(scroll);
append_page(new ConfigLocationGrid());
var clamp = new Adw.Clamp();
clamp.child = new ConfigLocationGroup();
DejaDup.set_margins(clamp, 12);
append_page(clamp);
}
}
......
......@@ -9,23 +9,27 @@ using GLib;
// A simple one-page assistant to ask where to restore from
public class AssistantLocation : Assistant
{
ConfigLocationGrid location_grid;
ConfigLocationGroup location_group;
construct
{
default_title = _("Restore From Where?");
modal = true;
destroy_with_parent = true;
location_grid = new ConfigLocationGrid(true);
location_grid.set_location_label(_("_Backup location"));
append_page(location_grid, Type.NORMAL, _("_Search"));
var clamp = new Adw.Clamp();
DejaDup.set_margins(clamp, 12);
location_group = new ConfigLocationGroup(true);
clamp.child = location_group;
append_page(clamp, Type.NORMAL, _("_Search"));
response.connect(handle_response);
}
void handle_response(int resp)
{
var backend = location_grid.get_backend();
var backend = location_group.get_backend();
destroy();
......
......@@ -34,11 +34,11 @@ public abstract class AssistantOperation : Assistant
Gtk.Label backend_install_packages;
Gtk.ProgressBar backend_install_progress;
Gtk.PasswordEntry nag_entry;
Gtk.PasswordEntry encrypt_entry;
Gtk.PasswordEntry confirm_entry;
Gtk.CheckButton encrypt_enabled;
Gtk.CheckButton encrypt_remember;
Adw.PasswordEntryRow nag_entry;
Adw.PasswordEntryRow encrypt_entry;
Adw.PasswordEntryRow confirm_entry;
SwitchRow encrypt_enabled;
SwitchRow encrypt_remember;
protected Gtk.Widget password_page {get; private set;}
protected Gtk.Widget nag_page {get; private set;}
protected bool nagged;
......@@ -335,127 +335,78 @@ public abstract class AssistantOperation : Assistant
protected Gtk.Widget make_password_page()
{
int rows = 0;
Gtk.Widget w, label;
var page = new Gtk.Grid();
page.row_spacing = 6;
page.column_spacing = 6;
var page = new Adw.Clamp();
DejaDup.set_margins(page, 12);
w = new Gtk.CheckButton.with_mnemonic(_("_Allow restoring without a password"));
page.attach(w, 0, rows, 3, 1);
encryption_choice_widgets.append(w);
++rows;
var group = new Adw.PreferencesGroup();
page.child = group;
encrypt_enabled = new Gtk.CheckButton.with_mnemonic(_("_Password-protect your backup"));
encrypt_enabled.group = w as Gtk.CheckButton;
encrypt_enabled = new SwitchRow();
encrypt_enabled.active = true; // always default to encrypted
page.attach(encrypt_enabled, 0, rows, 3, 1);
encrypt_enabled.subtitle = _("You will need your password to restore your files. You might want to write it down.");
encrypt_enabled.title = _("_Password-protect your backup");
encrypt_enabled.notify["active"].connect(check_password_validity);
group.add(encrypt_enabled);
encryption_choice_widgets.append(encrypt_enabled);
encrypt_enabled.toggled.connect(check_password_validity);
++rows;
w = new Gtk.Label(" "); // indent
page.attach(w, 0, rows, 1, 1);
encryption_choice_widgets.append(w);
w = new Gtk.Label(
_("You will need your password to restore your files. You might want to write it down.")
);
w.add_css_class("caption-heading");
w.set("xalign", 0.0f,
"max-width-chars", 25,
"wrap", true);
page.attach(w, 1, rows, 2, 1);
encrypt_enabled.bind_property("active", w, "sensitive", BindingFlags.SYNC_CREATE);
first_password_widgets.append(w);
++rows;
encrypt_entry = new Gtk.PasswordEntry();
encrypt_entry.hexpand = true;
encrypt_entry = new Adw.PasswordEntryRow();
encrypt_entry.activates_default = true;
encrypt_entry.show_peek_icon = true;
encrypt_entry.title = _("E_ncryption password");
encrypt_entry.use_underline = true;
encrypt_entry.changed.connect(check_password_validity);
label = new Gtk.Label(_("E_ncryption password"));
label.set("mnemonic-widget", encrypt_entry,
"use-underline", true,
"xalign", 1.0f);
page.attach(label, 1, rows, 1, 1);
page.attach(encrypt_entry, 2, rows, 1, 1);
encrypt_enabled.bind_property("active", encrypt_entry, "sensitive", BindingFlags.SYNC_CREATE);
encrypt_enabled.bind_property("active", label, "sensitive", BindingFlags.SYNC_CREATE);
++rows;
group.add(encrypt_entry);
// Add a confirmation entry if this is user's first time
confirm_entry = new Gtk.PasswordEntry();
confirm_entry.hexpand = true;
confirm_entry = new Adw.PasswordEntryRow();
confirm_entry.activates_default = true;
confirm_entry.show_peek_icon = true;
confirm_entry.title = _("Confir_m password");
confirm_entry.use_underline = true;
confirm_entry.changed.connect(check_password_validity);
label = new Gtk.Label(_("Confir_m password"));
label.set("mnemonic-widget", confirm_entry,
"use-underline", true,
"xalign", 1.0f);
page.attach(label, 1, rows, 1, 1);
page.attach(confirm_entry, 2, rows, 1, 1);
encrypt_enabled.bind_property("active", confirm_entry, "sensitive", BindingFlags.SYNC_CREATE);
encrypt_enabled.bind_property("active", label, "sensitive", BindingFlags.SYNC_CREATE);
++rows;
group.add(confirm_entry);
first_password_widgets.append(confirm_entry);
first_password_widgets.append(label);
w = new Gtk.CheckButton.with_mnemonic(_("_Remember password"));
w.halign = Gtk.Align.END;
page.attach(w, 1, rows, 2, 1);
encrypt_enabled.bind_property("active", w, "sensitive", BindingFlags.SYNC_CREATE);
++rows;
encrypt_remember = (Gtk.CheckButton)w;
encrypt_remember = new SwitchRow();
encrypt_remember.title = _("_Remember password");
encrypt_enabled.bind_property("active", encrypt_remember, "sensitive", BindingFlags.SYNC_CREATE);
group.add(encrypt_remember);
return page;
}
protected Gtk.Widget make_nag_page()
{
int rows = 0;
Gtk.Widget w;
var page = new Gtk.Grid();
page.row_spacing = 12;
page.column_spacing = 6;
var page = new Adw.Clamp();
DejaDup.set_margins(page, 12);
w = new Gtk.Label(_("In order to check that you will be able to retrieve your files in the case " +
"of an emergency, please enter your encryption password again to perform a " +
"brief restore test."));
w.set("xalign", 0.0f,
"max-width-chars", 25,
"wrap", true);
page.attach(w, 0, rows, 2, 1);
++rows;
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 12);
page.child = box;
var label = new Gtk.Label(_("In order to check that you will be able to retrieve your files in the case " +
"of an emergency, please enter your encryption password again to perform a " +
"brief restore test."));
label.wrap = true;
label.xalign = 0;
box.append(label);
nag_entry = new Gtk.PasswordEntry();
nag_entry.hexpand = true;
var group = new Adw.PreferencesGroup();
box.append(group);
nag_entry = new Adw.PasswordEntryRow();
nag_entry.activates_default = true;
nag_entry.show_peek_icon = true;
nag_entry.title =_("E_ncryption password");
nag_entry.use_underline = true;
nag_entry.changed.connect(check_nag_validity);
var label = new Gtk.Label(_("E_ncryption password"));
label.mnemonic_widget = nag_entry;
label.use_underline = true;
label.xalign = 0;
page.attach(label, 0, rows, 1, 1);
page.attach(nag_entry, 1, rows, 1, 1);
++rows;
group.add(nag_entry);
w = new Gtk.CheckButton.with_mnemonic(_("Test every two _months"));
page.attach(w, 0, rows, 2, 1);
((Gtk.CheckButton)w).active = true;
w.vexpand = true;
w.valign = Gtk.Align.END;
((Gtk.CheckButton)w).toggled.connect((button) => {
DejaDup.update_nag_time(!button.get_active());
var nag_row = new SwitchRow();
nag_row.active = true;
nag_row.title = _("Test every two _months");
nag_row.notify["active"].connect((row, spec) => {
DejaDup.update_nag_time(!((SwitchRow)row).active);
});
++rows;
group.add(nag_row);
return page;
}
......@@ -777,18 +728,30 @@ public abstract class AssistantOperation : Assistant
}
var passphrase = encrypt_entry.get_text();
if (passphrase == "") {
allow_forward(false);
return;
}
var passphrase_entered = passphrase != "";
if (confirm_entry.visible) {
var passphrase2 = confirm_entry.text;
var valid = (passphrase == passphrase2);
var valid = (passphrase == passphrase2) && passphrase_entered;
// The HIG recommends positive rather than negative feedback, but
// Settings uses negative feedback in a similar setting (see the password
// changer dialog) and if we use positive-only feedback, the user would
// not see why the forward button isn't active until they have already
// solved why not by filling the entries.
if (passphrase_entered) {
encrypt_entry.remove_css_class("error");
} else {
encrypt_entry.add_css_class("error");
}
if (valid) {
confirm_entry.remove_css_class("error");
} else {
confirm_entry.add_css_class("error");
}
allow_forward(valid);
}
else
allow_forward(true);
allow_forward(passphrase_entered);
}
void configure_password_page(bool first)
......
......@@ -37,9 +37,7 @@ public class AssistantRestore : AssistantOperation
Gtk.ProgressBar status_progress_bar;
uint status_timeout_id;
TimeCombo date_combo;
Gtk.CheckButton cust_radio;
Gtk.Label cust_label;
FolderChooserButton cust_button;
Adw.EntryRow cust_entry_row;
Gtk.Grid confirm_table;
Gtk.Image confirm_storage_image;
Gtk.Label confirm_storage_label;
......@@ -106,9 +104,18 @@ public class AssistantRestore : AssistantOperation
return page;
}
void allow_restore_location_forward(bool allowed)
{
allow_forward(allowed);
if (allowed)
cust_entry_row.remove_css_class("error");
else
cust_entry_row.add_css_class("error");
}
void restore_location_updated()
{
allow_forward(restore_location != null);
allow_restore_location_forward(restore_location != null);
bad_files_grid.visible = false;
if (restore_location == null || tree == null)
......@@ -129,16 +136,36 @@ public class AssistantRestore : AssistantOperation
bad_files_grid.visible = true;
if (restore_files != null || all_bad)
allow_forward(false); // on basis that they really want these specific files
allow_restore_location_forward(false); // on basis that they really want these specific files
}
}
string? get_abs_path(string user_path)
{
if (user_path == "")
return null;
else if (!Path.is_absolute(user_path))
return Path.build_filename(Environment.get_home_dir(), user_path);
else
return user_path;
}
Gtk.Widget make_restore_dest_page()
{
var orig_row = new Adw.ActionRow();
var orig_radio = new Gtk.CheckButton();
orig_radio.label = _("Restore files to _original locations");
orig_radio.use_underline = true;
var cust_row = new Adw.ActionRow();
var cust_radio = new Gtk.CheckButton();
cust_entry_row = new Adw.EntryRow();
var open_button = new FolderChooserButton();
orig_row.title = _("Restore files to _original locations");
orig_row.use_underline = true;
orig_row.activatable_widget = orig_radio;
orig_row.add_prefix(orig_radio);
orig_radio.active = true;
orig_radio.can_focus = false;
orig_radio.toggled.connect((r) => {
if (r.active) {
restore_location = "/";
......@@ -146,41 +173,38 @@ public class AssistantRestore : AssistantOperation
}
});
cust_radio = new Gtk.CheckButton();
cust_radio.label = _("Restore to _specific folder");
cust_radio.use_underline = true;
cust_row.title = _("Restore to _specific folder");
cust_row.use_underline = true;
cust_row.activatable_widget = cust_radio;
cust_row.add_prefix(cust_radio);
cust_radio.group = orig_radio;
cust_radio.can_focus = false;
cust_radio.toggled.connect((r) => {
if (r.active) {
restore_location = cust_button.file.get_path();
restore_location = get_abs_path(cust_entry_row.text);
restore_location_updated();
}
cust_button.sensitive = r.active;
cust_entry_row.sensitive = r.active;
});
cust_label = new Gtk.Label("");
cust_label.ellipsize = Pango.EllipsizeMode.MIDDLE;
cust_label.hexpand = true;
cust_label.xalign = 0;
var attrs = new Pango.AttrList();
attrs.insert(Pango.attr_weight_new(Pango.Weight.BOLD));
cust_label.attributes = attrs;
var cust_radio_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
cust_radio_box.append(cust_radio);
cust_radio_box.append(cust_label);
cust_button = new FolderChooserButton();
cust_button.margin_start = 24;
cust_button.halign = Gtk.Align.START;
cust_button.sensitive = false;
cust_button.file_selected.connect(() => {
cust_radio.label = _("Restore to _specific folder:") + " ";
cust_label.label = cust_button.path;
restore_location = cust_button.file.get_path();
cust_entry_row.title = _("_Folder");
cust_entry_row.input_hints = Gtk.InputHints.NO_SPELLCHECK;
cust_entry_row.sensitive = false;
cust_entry_row.use_underline = true;
cust_entry_row.activates_default = true;
cust_entry_row.add_suffix(open_button);
cust_entry_row.changed.connect(() => {
restore_location = get_abs_path(cust_entry_row.text);
restore_location_updated();
});
open_button.valign = Gtk.Align.CENTER;
open_button.add_css_class("flat");
open_button.file_selected.connect(() => {
cust_entry_row.text = open_button.path;
});
var bad_icon = new Gtk.Image.from_icon_name("dialog-warning");
bad_icon.valign = Gtk.Align.START;
......@@ -208,16 +232,16 @@ public class AssistantRestore : AssistantOperation
bad_files_grid.attach(bad_header, 1, 0);
bad_files_grid.attach(bad_files_label, 0, 1, 2, 1);
var page = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
DejaDup.set_margins(page, 12);
page.append(orig_radio);
page.append(cust_radio_box);
page.append(cust_button);
page.append(bad_files_grid);
var group = new Adw.PreferencesGroup();
DejaDup.set_margins(group, 12);
group.add(orig_row);
group.add(cust_row);
group.add(cust_entry_row);
group.add(bad_files_grid);
var scroll = new Gtk.ScrolledWindow();
scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
scroll.child = page;
scroll.child = group;
return scroll;
}
......
/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*-
*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: Michael Terry
*/
using GLib;
public class ConfigAutoBackup: ConfigSwitch
{
construct {
var settings = DejaDup.get_settings();
settings.bind(DejaDup.PERIODIC_KEY, this.toggle, "active", SettingsBindFlags.GET);
this.toggle.state_set.connect(on_state_set);
}
bool on_state_set(bool state)
{
if (state) {
var window = this.root as Gtk.Window;
if (window == null) {
return true; // can happen if this switch wasn't finalized
}
Background.request_autostart.begin(window, (obj, res) => {
if (Background.request_autostart.end(res)) {
this.toggle.state = true; // finish state set
set_periodic(true);
} else {
this.toggle.active = false; // flip switch back to unset mode
}
});
return true; // delay setting of state
}
set_periodic(false);
return false;
}
static void set_periodic(bool state)
{
var settings = DejaDup.get_settings();
settings.set_boolean(DejaDup.PERIODIC_KEY, state);
}
}
......@@ -6,8 +6,7 @@
using GLib;
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigAutoBackupRow.ui")]
public class ConfigAutoBackupRow : Adw.ActionRow
public class ConfigAutoBackupRow : SwitchRow
{
Settings settings;
construct {
......@@ -15,7 +14,11 @@ public class ConfigAutoBackupRow : Adw.ActionRow
settings.changed[DejaDup.PERIODIC_KEY].connect(update_label);
settings.changed[DejaDup.PERIODIC_PERIOD_KEY].connect(update_label);
settings.changed[DejaDup.LAST_BACKUP_KEY].connect(update_label);
settings.bind(DejaDup.PERIODIC_KEY, this, "active", SettingsBindFlags.GET);
state_set.connect(on_state_set);
title = _("Back Up _Automatically");
update_label();
}
......@@ -91,4 +94,33 @@ public class ConfigAutoBackupRow : Adw.ActionRow
next = new DateTime.now_local().add(DejaDup.get_day() * 7);
subtitle = pretty_next_name(next, periodic);
}
bool on_state_set(bool state)
{
if (state) {
var window = this.root as Gtk.Window;
if (window == null) {
return true; // can happen if this switch wasn't finalized
}
Background.request_autostart.begin(window, (obj, res) => {
if (Background.request_autostart.end(res)) {
this.state = true; // finish state set
set_periodic(true);
} else {
this.active = false; // flip switch back to unset mode
}
});
return true; // delay setting of state
}
set_periodic(false);
return false;
}
static void set_periodic(bool state)
{
var settings = DejaDup.get_settings();
settings.set_boolean(DejaDup.PERIODIC_KEY, state);
}
}
......@@ -6,8 +6,8 @@
using GLib;
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigFolderList.ui")]
public class ConfigFolderList : Adw.PreferencesGroup
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigFolderGroup.ui")]
public class ConfigFolderGroup : Adw.PreferencesGroup
{
public string key {get; construct;}
public bool check_access {get; construct;}
......
......@@ -9,14 +9,4 @@ using GLib;
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigFolderPage.ui")]
public class ConfigFolderPage : Adw.PreferencesPage
{
#if HAS_ADWAITA_1_1
[GtkChild]
unowned ConfigFolderList exclude_list;
#endif
construct {
#if HAS_ADWAITA_1_1
exclude_list.header_suffix = new ExcludeHelpButton();
#endif
}
}
......@@ -6,20 +6,15 @@
using GLib;
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigLocationGrid.ui")]
public class ConfigLocationGrid : Gtk.Grid
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigLocationGroup.ui")]
public class ConfigLocationGroup : DynamicPreferencesGroup
{
public bool read_only {get; construct;}
public ConfigLocationGrid(bool read_only = false) {
public ConfigLocationGroup(bool read_only = false) {
Object(read_only: read_only);
}
public void set_location_label(string label)
{
location_label.label = label;
}
public DejaDup.Backend get_backend()
{
string name = DejaDup.Backend.get_key_name(settings);
......@@ -40,30 +35,28 @@ public class ConfigLocationGrid : Gtk.Grid
}
[GtkChild]
unowned Gtk.Label location_label;
[GtkChild]
unowned Gtk.Stack location_stack;
unowned ConfigLocationRow combo;
[GtkChild]
unowned Gtk.Entry google_folder;
unowned Adw.EntryRow google_folder;
[GtkChild]
unowned Gtk.Button google_reset;
[GtkChild]
unowned Gtk.Entry microsoft_folder;
unowned Adw.EntryRow microsoft_folder;
[GtkChild]
unowned Gtk.Button microsoft_reset;
[GtkChild]
unowned ConfigServerEntry remote_address;
unowned Adw.EntryRow remote_address;
[GtkChild]
unowned Gtk.Entry remote_folder;
unowned Adw.EntryRow remote_folder;
[GtkChild]
unowned Gtk.Entry drive_folder;
unowned Adw.EntryRow drive_folder;
[GtkChild]
unowned Gtk.Entry local_folder;
unowned Adw.EntryRow local_folder;
[GtkChild]
unowned FolderChooserButton local_browse;
......@@ -76,41 +69,41 @@ public class ConfigLocationGrid : Gtk.Grid
DejaDup.FilteredSettings microsoft_settings;
DejaDup.FilteredSettings local_settings;
DejaDup.FilteredSettings remote_settings;
ConfigLocationCombo combo;
construct {
settings = new DejaDup.FilteredSettings(null, read_only);
drive_settings = new DejaDup.FilteredSettings(DejaDup.DRIVE_ROOT, read_only);
bind_folder(drive_settings, DejaDup.DRIVE_FOLDER_KEY, drive_folder, false);
// Google
google_settings = new DejaDup.FilteredSettings(DejaDup.GOOGLE_ROOT, read_only);
bind_folder(google_settings, DejaDup.GOOGLE_FOLDER_KEY, google_folder, false);
set_up_google_reset.begin();
// Microsoft
microsoft_settings = new DejaDup.FilteredSettings(DejaDup.MICROSOFT_ROOT, read_only);
bind_folder(microsoft_settings, DejaDup.MICROSOFT_FOLDER_KEY, microsoft_folder, false);
set_up_microsoft_reset.begin();
local_settings = new DejaDup.FilteredSettings(DejaDup.LOCAL_ROOT, read_only);
bind_folder(local_settings, DejaDup.LOCAL_FOLDER_KEY, local_folder, true);
// Remote
remote_settings = new DejaDup.FilteredSettings(DejaDup.REMOTE_ROOT, read_only);
bind_folder(remote_settings, DejaDup.REMOTE_FOLDER_KEY, remote_folder, true);
remote_settings.bind(DejaDup.REMOTE_URI_KEY, remote_address,
"text", SettingsBindFlags.DEFAULT);
combo = new ConfigLocationCombo(settings, drive_settings);
combo.hexpand = true;
attach(combo, 1, 0);
location_label.mnemonic_widget = combo;
// Drive
drive_settings = new DejaDup.FilteredSettings(DejaDup.DRIVE_ROOT, read_only);
bind_folder(drive_settings, DejaDup.DRIVE_FOLDER_KEY, drive_folder, false);
// Local
local_settings = new DejaDup.FilteredSettings(DejaDup.LOCAL_ROOT, read_only);
bind_folder(local_settings, DejaDup.LOCAL_FOLDER_KEY, local_folder, true);
combo.setup(settings, drive_settings);
combo.notify["selected-item"].connect(update_stack);
update_stack();
}
void update_stack()
{
var item = combo.selected_item;
var item = combo.selected_item as ConfigLocationRow.Item;
if (item == null)
return;
......@@ -122,10 +115,10 @@ public class ConfigLocationGrid : Gtk.Grid
if (page == "unsupported")
unsupported_label.label = support_explanation;
location_stack.visible_child_name = page;
mode = page;
}
void bind_folder(Settings settings, string key, Gtk.Entry entry, bool allow_abs)
void bind_folder(Settings settings, string key, Adw.EntryRow entry, bool allow_abs)
{
settings.bind_with_mapping(key, entry, "text",
SettingsBindFlags.DEFAULT, get_folder_mapping, set_identity_mapping,
......@@ -136,6 +129,8 @@ public class ConfigLocationGrid : Gtk.Grid
{
var allow_abs = (bool)int.from_pointer(data);
var folder = DejaDup.process_folder_key(variant.get_string(), allow_abs, null);
if (DejaDup.in_demo_mode())
folder = "hostname";
val.set_string(folder);
return true;
}
......@@ -168,9 +163,7 @@ public class ConfigLocationGrid : Gtk.Grid
{
var backend = new DejaDup.BackendGoogle(google_settings);
var token = yield backend.lookup_refresh_token();
if (token != null) {
google_reset.visible = true;
}
google_reset.visible = token != null && !DejaDup.in_demo_mode();
}
[GtkCallback]
......@@ -185,8 +178,6 @@ public class ConfigLocationGrid : Gtk.Grid
{
var backend = new DejaDup.BackendMicrosoft(microsoft_settings);
var token = yield backend.lookup_refresh_token();
if (token != null) {
microsoft_reset.visible = true;
}
microsoft_reset.visible = token != null;
}
}
......@@ -6,17 +6,11 @@
using GLib;
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigLocationCombo.ui")]
public class ConfigLocationCombo : Gtk.Box
[GtkTemplate (ui = "/org/gnome/DejaDup/ConfigLocationRow.ui")]
public class ConfigLocationRow : Adw.ComboRow
{
public Item selected_item {get; protected set;}
public DejaDup.FilteredSettings settings {get; construct;}
public DejaDup.FilteredSettings drive_settings {get; construct;}
public ConfigLocationCombo(DejaDup.FilteredSettings settings,
DejaDup.FilteredSettings drive_settings) {
Object(settings: settings, drive_settings: drive_settings);
}
public DejaDup.FilteredSettings settings {get; private set;}
public DejaDup.FilteredSettings drive_settings {get; private set;}
public class Item : Object {
public Icon icon {get; set;}
......@@ -41,18 +35,17 @@ public class ConfigLocationCombo : Gtk.Box
LOCAL,
}
[GtkChild]
unowned Gtk.DropDown combo;
ListStore store;
construct {
// Here we have a model wrapped inside a sortable model. This is so we
// can keep indices around for the inner model while the outer model appears
// nice and sorted to users.
store = new ListStore(typeof(Item));
combo.model = store;
model = store;
}
combo.bind_property("selected-item", this, "selected-item", BindingFlags.SYNC_CREATE);
public void setup(DejaDup.FilteredSettings settings,
DejaDup.FilteredSettings drive_settings)
{
this.settings = settings;
this.drive_settings = drive_settings;
// *** Basic entries ***
......@@ -91,17 +84,12 @@ public class ConfigLocationCombo : Gtk.Box
// *** Now bind our combo to settings ***
settings.bind_with_mapping(DejaDup.BACKEND_KEY,
combo, "selected",
this, "selected",
SettingsBindFlags.DEFAULT,
get_mapping, set_mapping,
this.ref(), Object.unref);
}
[GtkCallback]
bool on_mnemonic_activate(bool group_cycling) {
return combo.mnemonic_activate(group_cycling);
}
void add_entry(string id, string? icon, string label, Group group, DejaDup.Backend.Kind kind)
{
// If this backend is unsupported, only add it to the combo if it's currently selected
......@@ -223,10 +211,13 @@ public class ConfigLocationCombo : Gtk.Box
static bool get_mapping(Value val, Variant variant, void *data)
{
var self = (ConfigLocationCombo)data;
var self = (ConfigLocationRow)data;
uint position;
var id = variant.get_string();
if (DejaDup.in_demo_mode())
id = "google";
if (id == "drive") {
position = self.add_saved_volume();
}
......@@ -243,7 +234,7 @@ public class ConfigLocationCombo : Gtk.Box
static Variant set_mapping(Value val, VariantType expected_type, void *data)
{
var self = (ConfigLocationCombo)data;
var self = (ConfigLocationRow)data;
var position = val.get_uint();
var item = (Item)self.store.get_item(position);
var id = item.id;
......@@ -262,7 +253,7 @@ public class ConfigLocationCombo : Gtk.Box
{
var position = add_saved_volume();
if (position != uint.MAX)
combo.selected = position;
selected = position;
}
void set_volume_info(string uuid)
......