From be3ed978e2ad77c1197ccb5a6e887ffaeec12057 Mon Sep 17 00:00:00 2001 From: Niels De Graef Date: Fri, 8 Aug 2025 09:44:29 +0200 Subject: [PATCH] Draft: Port to GTK4 --- .gitlab-ci.yml | 7 +- meson.build | 39 +- org.gnome.Geary.json | 16 + po/POTFILES.in | 2 +- .../accounts/accounts-editor-add-pane.vala | 516 +++-------- .../accounts/accounts-editor-edit-pane.vala | 467 +++------- .../accounts/accounts-editor-list-pane.vala | 367 +++++--- src/client/accounts/accounts-editor-row.vala | 311 +++---- .../accounts-editor-servers-pane.vala | 465 +++------- src/client/accounts/accounts-editor.vala | 170 +--- .../accounts-mailbox-editor-dialog.vala | 128 +++ .../accounts-service-information-widget.vala | 173 ++++ .../accounts/accounts-signature-web-view.vala | 3 +- .../accounts/accounts-tls-combo-row.vala | 55 ++ .../application-attachment-manager.vala | 140 ++- .../application-certificate-manager.vala | 4 +- .../application/application-client.vala | 116 ++- .../application-contact-store.vala | 12 +- .../application/application-contact.vala | 40 +- .../application/application-controller.vala | 91 +- .../application-database-manager.vala | 22 +- .../application/application-main-window.vala | 857 +++++------------- ...plication-notification-plugin-context.vala | 4 +- .../application-plugin-manager.vala | 26 +- .../components-attachment-pane.vala | 219 ++--- .../components-conversation-actions.vala | 54 +- .../components/components-entry-undo.vala | 19 +- .../components-headerbar-application.vala | 34 - ...omponents-headerbar-conversation-list.vala | 44 - .../components-headerbar-conversation.vala | 50 +- .../components-in-app-notification.vala | 75 -- .../components/components-info-bar-stack.vala | 10 +- .../components/components-info-bar.vala | 49 +- .../components-inspector-error-view.vala | 2 +- .../components-inspector-log-view.vala | 213 ++--- .../components-inspector-system-view.vala | 55 +- .../components/components-inspector.vala | 67 +- .../components-placeholder-pane.vala | 4 +- .../components-preferences-dialog.vala | 159 ++++ .../components-preferences-window.vala | 294 ------ .../components-problem-report-info-bar.vala | 5 +- src/client/components/components-reflow-box.c | 6 +- .../components/components-search-bar.vala | 31 +- .../components-validator-group.vala | 91 ++ .../components/components-validator.vala | 356 ++++---- .../components/components-web-view.vala | 82 +- src/client/components/folder-popover.vala | 7 +- src/client/components/icon-factory.vala | 142 --- .../components/monitored-progress-bar.vala | 17 +- src/client/components/monitored-spinner.vala | 18 +- .../composer/composer-addresses-row.vala | 457 ++++++++++ .../composer-application-interface.vala | 1 + src/client/composer/composer-box.vala | 14 +- src/client/composer/composer-editor.vala | 92 +- src/client/composer/composer-email-entry.vala | 24 +- src/client/composer/composer-embed.vala | 38 +- src/client/composer/composer-headerbar.vala | 21 +- .../composer/composer-link-popover.vala | 17 +- src/client/composer/composer-web-view.vala | 21 +- src/client/composer/composer-widget.vala | 779 +++++++--------- src/client/composer/composer-window.vala | 62 +- .../composer/contact-entry-completion.vala | 5 +- src/client/composer/spell-check-popover.vala | 62 +- .../conversation-list-row.vala | 8 +- .../conversation-list-view.vala | 143 +-- .../conversation-contact-popover.vala | 67 +- .../conversation-email.vala | 57 +- .../conversation-list-box.vala | 251 +++-- .../conversation-message.vala | 286 +++--- .../conversation-viewer.vala | 124 +-- .../conversation-web-view.vala | 63 +- src/client/dialogs/alert-dialog.vala | 128 --- src/client/dialogs/attachment-dialog.vala | 104 --- .../dialogs/certificate-warning-dialog.vala | 57 +- .../dialogs-problem-details-dialog.vala | 87 +- src/client/dialogs/password-dialog.vala | 70 +- .../folder-list/folder-list-folder-entry.vala | 3 + src/client/folder-list/folder-list-tree.vala | 37 +- src/client/meson.build | 21 +- .../desktop-notifications.vala | 48 +- src/client/plugin/mail-merge/mail-merge.vala | 28 +- src/client/plugin/mail-merge/meson.build | 2 +- src/client/plugin/meson.build | 5 +- src/client/sidebar/sidebar-common.vala | 3 +- .../sidebar/sidebar-count-cell-renderer.vala | 21 +- src/client/sidebar/sidebar-entry.vala | 6 +- src/client/sidebar/sidebar-tree.vala | 176 ++-- src/client/util/util-contact.vala | 3 +- src/client/util/util-gtk.vala | 12 +- .../web-process/web-process-extension.vala | 18 +- src/console/imap-console.ui | 51 ++ src/console/main.vala | 61 +- src/console/meson.build | 9 +- .../org.gnome.GearyConsole.gresource.xml | 6 + src/engine/api/geary-credentials.vala | 21 + src/engine/api/geary-service-information.vala | 15 + src/engine/util/util-logging.vala | 2 +- src/mailer/meson.build | 2 +- src/meson.build | 21 +- subprojects/libhandy.wrap | 5 - .../components-web-view-test-case.vala | 14 +- .../components/components-web-view-test.vala | 6 +- .../composer/composer-web-view-test.vala | 48 +- .../client/composer/composer-widget-test.vala | 8 + test/js/components-page-state-test.vala | 8 +- test/js/composer-page-state-test.vala | 8 +- test/js/conversation-page-state-test.vala | 4 +- test/meson.build | 2 +- test/test-client.vala | 15 +- test/test-js.vala | 15 +- ui/accounts-editor-account-list-row.ui | 41 + ui/accounts-mailbox-editor-dialog.ui | 81 ++ ui/accounts-service-information-widget.ui | 64 ++ ui/accounts-tls-combo-row.ui | 18 + ui/accounts_editor.ui | 57 +- ui/accounts_editor_add_pane.ui | 281 ++---- ui/accounts_editor_edit_pane.ui | 387 +++----- ui/accounts_editor_list_pane.ui | 176 ++-- ui/accounts_editor_servers_pane.ui | 269 ++---- ui/application-main-window.ui | 518 +++++++---- ui/certificate-warning-dialog.ui | 65 ++ ui/certificate_warning_dialog.glade | 201 ---- ui/components-attachment-pane.ui | 208 ++--- ui/components-attachment-view.ui | 32 +- ui/components-conversation-actions.ui | 102 +-- ui/components-headerbar-application.ui | 36 - ui/components-headerbar-conversation-list.ui | 84 -- ui/components-headerbar-conversation.ui | 78 +- ui/components-in-app-notification.ui | 51 -- ui/components-info-bar.ui | 23 +- ui/components-inspector-error-view.ui | 39 +- ui/components-inspector-log-view.ui | 168 ++-- ui/components-inspector-system-view.ui | 50 +- ui/components-inspector.ui | 26 +- ui/components-menu-application.ui | 30 - ui/components-placeholder-pane.ui | 22 +- ui/components-preferences-dialog.ui | 68 ++ ui/composer-editor.ui | 424 ++------- ui/composer-headerbar.ui | 243 ++--- ui/composer-link-popover.ui | 57 +- ui/composer-widget.ui | 249 +++-- ui/conversation-contact-popover.ui | 292 ++---- ui/conversation-email.ui | 108 +-- ui/conversation-list-row.ui | 201 ++-- ui/conversation-list-view.ui | 24 +- ui/conversation-message.ui | 651 +++++-------- ui/conversation-viewer.ui | 350 +++---- ui/folder-popover.ui | 23 - ui/org.gnome.Geary.gresource.xml | 40 +- ui/password-dialog.glade | 225 ----- ui/password-dialog.ui | 52 ++ ui/problem-details-dialog.ui | 131 +-- ui/{geary.css => style.css} | 44 +- 153 files changed, 6673 insertions(+), 9492 deletions(-) create mode 100644 src/client/accounts/accounts-mailbox-editor-dialog.vala create mode 100644 src/client/accounts/accounts-service-information-widget.vala create mode 100644 src/client/accounts/accounts-tls-combo-row.vala delete mode 100644 src/client/components/components-headerbar-application.vala delete mode 100644 src/client/components/components-headerbar-conversation-list.vala delete mode 100644 src/client/components/components-in-app-notification.vala create mode 100644 src/client/components/components-preferences-dialog.vala delete mode 100644 src/client/components/components-preferences-window.vala create mode 100644 src/client/components/components-validator-group.vala delete mode 100644 src/client/components/icon-factory.vala create mode 100644 src/client/composer/composer-addresses-row.vala delete mode 100644 src/client/dialogs/alert-dialog.vala delete mode 100644 src/client/dialogs/attachment-dialog.vala create mode 100644 src/console/imap-console.ui create mode 100644 src/console/org.gnome.GearyConsole.gresource.xml delete mode 100644 subprojects/libhandy.wrap create mode 100644 ui/accounts-editor-account-list-row.ui create mode 100644 ui/accounts-mailbox-editor-dialog.ui create mode 100644 ui/accounts-service-information-widget.ui create mode 100644 ui/accounts-tls-combo-row.ui create mode 100644 ui/certificate-warning-dialog.ui delete mode 100644 ui/certificate_warning_dialog.glade delete mode 100644 ui/components-headerbar-application.ui delete mode 100644 ui/components-headerbar-conversation-list.ui delete mode 100644 ui/components-in-app-notification.ui delete mode 100644 ui/components-menu-application.ui create mode 100644 ui/components-preferences-dialog.ui delete mode 100644 ui/password-dialog.glade create mode 100644 ui/password-dialog.ui rename ui/{geary.css => style.css} (91%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8e91f8b..c482d84b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -67,7 +67,7 @@ build.container.fedora@x86_64: gsettings-desktop-schemas gsound-devel gspell-devel - gtk3-devel + gtk4-devel iso-codes-devel itstool json-glib-devel @@ -76,6 +76,7 @@ build.container.fedora@x86_64: libicu-devel libpeas-devel libsecret-devel + libspelling-devel libstemmer-devel libunwind-devel libxml2-devel @@ -93,8 +94,8 @@ build.container.fedora@x86_64: # When branching a stable release, change 'main' to the # release branch name to ensure that a new image will # be created, tailored for the stable branch. - BRANCH_NAME: 'main' - CONTAINER_TAG: '2025-12-12.0' + BRANCH_NAME: 'nielsdg/gtk4' + CONTAINER_TAG: '2025-12-15.0' FEDORA_VERSION: latest # Derive FDO variables from this automatically. # DO NOT edit, instead change the variables above diff --git a/meson.build b/meson.build index 59b4ba7e..01cb6af5 100644 --- a/meson.build +++ b/meson.build @@ -52,10 +52,10 @@ valac = meson.get_compiler('vala') # Required libraries and other dependencies # -target_glib = '2.74' -target_gtk = '3.24.24' +target_glib = '2.80' +target_gtk = '4.16.0' target_vala = '0.56' -target_webkit = '2.30' +target_webkit = '2.40' if not valac.version().version_compare('>=' + target_vala) error('Vala does not meet minimum required version: ' + target_vala) @@ -64,9 +64,9 @@ endif # Primary deps glib = dependency('glib-2.0', version: '>=' + target_glib) gmime = dependency('gmime-3.0', version: '>= 3.2.4') -gtk = dependency('gtk+-3.0', version: '>=' + target_gtk) +gtk = dependency('gtk4', version: '>=' + target_gtk) sqlite = dependency('sqlite3', version: '>= 3.24') -webkit2gtk = dependency('webkit2gtk-4.1', version: '>=' + target_webkit) +webkitgtk = dependency('webkitgtk-6.0', version: '>=' + target_webkit) # Secondary deps - keep sorted alphabetically cairo = dependency('cairo') @@ -74,18 +74,17 @@ enchant = dependency('enchant-2', version: '>=2.1') folks = dependency('folks', version: '>=0.11') gck = dependency('gck-2') gcr = dependency('gcr-4') -gdk = dependency('gdk-3.0', version: '>=' + target_gtk) gee = dependency('gee-0.8', version: '>= 0.8.5') gio = dependency('gio-2.0', version: '>=' + target_glib) goa = dependency('goa-1.0') gsound = dependency('gsound') -gspell = dependency('gspell-1') +libspelling = dependency('libspelling-1') gthread = dependency('gthread-2.0', version: '>=' + target_glib) icu_uc = dependency('icu-uc', version: '>=60') iso_codes = dependency('iso-codes') -javascriptcoregtk = dependency('javascriptcoregtk-4.1', version: '>=' + target_webkit) +javascriptcoregtk = dependency('javascriptcoregtk-6.0', version: '>=' + target_webkit) json_glib = dependency('json-glib-1.0', version: '>= 1.0') -libhandy = dependency('libhandy-1', version: '>= 1.6', required: false) +libadwaita = dependency('libadwaita-1', version: '>= 1.7') libmath = cc.find_library('m') libpeas = dependency('libpeas-2') libsecret = dependency('libsecret-1', version: '>= 0.11') @@ -100,7 +99,7 @@ libunwind_generic_dep = dependency( libxml = dependency('libxml-2.0', version: '>= 2.7.8') libytnef = dependency('libytnef', version: '>= 1.9.3', required: get_option('tnef')) posix = valac.find_library('posix') -webkit2gtk_web_extension = dependency('webkit2gtk-web-extension-4.1', version: '>=' + target_webkit) +webkitgtk_web_extension = dependency('webkitgtk-web-process-extension-6.0', version: '>=' + target_webkit) # System dependencies above ensures appropriate versions for the # following libraries, but the declared dependency is what we actually @@ -133,26 +132,6 @@ libstemmer = declare_dependency( ], ) -# Required until libhandy 1.2.1 is GA -libhandy_vapi = '' -if not libhandy.found() - libhandy_project = subproject( - 'libhandy', - default_options: [ - 'examples=false', - 'package_subdir=geary', - 'tests=false', - ] - ) - libhandy = declare_dependency( - dependencies: [ - libhandy_project.get_variable('libhandy_dep'), - libhandy_project.get_variable('libhandy_vapi') - ] - ) - libhandy_vapi = meson.project_build_root() / 'subprojects' / 'libhandy' / 'src' -endif - # Optional dependencies appstreamcli = find_program('appstreamcli', required: false) desktop_file_validate = find_program('desktop-file-validate', required: false) diff --git a/org.gnome.Geary.json b/org.gnome.Geary.json index f4ddf12a..7252a43b 100644 --- a/org.gnome.Geary.json +++ b/org.gnome.Geary.json @@ -278,6 +278,22 @@ } ] }, + { + "name": "libspelling", + "buildsystem": "meson", + "config-opts": [ + "-Dintrospection=enabled", + "-Dvapi=true", + "-Ddocs=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/GNOME/libspelling.git", + "branch": "main" + } + ] + }, { "name": "geary", "buildsystem": "meson", diff --git a/po/POTFILES.in b/po/POTFILES.in index 7a6d5aef..2b94ea6d 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -474,5 +474,5 @@ ui/conversation-viewer.ui ui/find_bar.glade ui/folder-popover.ui ui/gtk/help-overlay.ui -ui/password-dialog.glade +ui/password-dialog.ui ui/problem-details-dialog.ui diff --git a/src/client/accounts/accounts-editor-add-pane.vala b/src/client/accounts/accounts-editor-add-pane.vala index eaf1533e..5a8d82e5 100644 --- a/src/client/accounts/accounts-editor-add-pane.vala +++ b/src/client/accounts/accounts-editor-add-pane.vala @@ -9,25 +9,21 @@ * An account editor pane for adding a new account. */ [GtkTemplate (ui = "/org/gnome/Geary/accounts_editor_add_pane.ui")] -internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { +internal class Accounts.EditorAddPane : Accounts.EditorPane { - internal Gtk.Widget initial_widget { - get { return this.real_name.value; } - } - /** {@inheritDoc} */ - internal bool is_operation_running { + internal override bool is_operation_running { get { return !this.sensitive; } protected set { update_operation_ui(value); } } /** {@inheritDoc} */ - internal GLib.Cancellable? op_cancellable { + internal override Cancellable? op_cancellable { get; protected set; default = new GLib.Cancellable(); } - protected weak Accounts.Editor editor { get; set; } + protected override weak Accounts.Editor editor { get; set; } private Geary.ServiceProvider provider; @@ -35,98 +31,55 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { private Geary.Engine engine; - [GtkChild] private unowned Gtk.HeaderBar header; + [GtkChild] private unowned Adw.HeaderBar header; [GtkChild] private unowned Gtk.Stack stack; - [GtkChild] private unowned Gtk.Adjustment pane_adjustment; + [GtkChild] private unowned Adw.PreferencesGroup details_list; + [GtkChild] private unowned Adw.EntryRow name_row; + [GtkChild] private unowned Adw.EntryRow email_row; + [GtkChild] private unowned Components.Validator email_validator; - [GtkChild] private unowned Gtk.ListBox details_list; - - [GtkChild] private unowned Gtk.ListBox receiving_list; - - [GtkChild] private unowned Gtk.ListBox sending_list; + [GtkChild] private unowned ServiceInformationWidget receiving_service_widget; + [GtkChild] private unowned ServiceInformationWidget sending_service_widget; [GtkChild] private unowned Gtk.Button action_button; + [GtkChild] private unowned Adw.Spinner action_spinner; - [GtkChild] private unowned Gtk.Button back_button; - - [GtkChild] private unowned Gtk.Spinner action_spinner; - - private NameRow real_name; - private EmailRow email = new EmailRow(); private string last_valid_email = ""; private string last_valid_hostname = ""; + //XXX if this is set, we shuld hide the IMAP/SMTP hostnames/auth + private bool did_auto_config { get; private set; default = false; } private GLib.Cancellable auto_config_cancellable = new GLib.Cancellable(); - private HostnameRow imap_hostname = new HostnameRow(Geary.Protocol.IMAP); - private TransportSecurityRow imap_tls = new TransportSecurityRow(); - private LoginRow imap_login = new LoginRow(); - private PasswordRow imap_password = new PasswordRow(); - - private HostnameRow smtp_hostname = new HostnameRow(Geary.Protocol.SMTP); - private TransportSecurityRow smtp_tls = new TransportSecurityRow(); - private OutgoingAuthRow smtp_auth = new OutgoingAuthRow(); - private LoginRow smtp_login = new LoginRow(); - private PasswordRow smtp_password = new PasswordRow(); - private bool controls_valid = false; + public Components.ValidatorGroup validators { get; construct set; } + + + static construct { + typeof(Components.ValidatorGroup).ensure(); + typeof(Components.Validator).ensure(); + typeof(Components.EmailValidator).ensure(); + } internal EditorAddPane(Editor editor) { - this.editor = editor; + Object(editor: editor); + this.provider = Geary.ServiceProvider.OTHER; this.accounts = editor.application.controller.account_manager; this.engine = editor.application.engine; - this.stack.set_focus_vadjustment(this.pane_adjustment); + this.name_row.text = this.accounts.get_account_name(); + //XXX GTK4 make sure it's validated immediately - this.details_list.set_header_func(Editor.seperator_headers); - this.receiving_list.set_header_func(Editor.seperator_headers); - this.sending_list.set_header_func(Editor.seperator_headers); + this.receiving_service_widget.service = new_imap_service(); + this.sending_service_widget.service = new_smtp_service(); - this.real_name = new NameRow(this.accounts.get_account_name()); - - this.details_list.add(this.real_name); - this.details_list.add(this.email); - - this.real_name.validator.state_changed.connect(on_validated); - this.real_name.value.activate.connect(on_activated); - this.email.validator.state_changed.connect(on_validated); - this.email.value.activate.connect(on_activated); - this.email.value.changed.connect(on_email_changed); - - this.imap_hostname.validator.state_changed.connect(on_validated); - this.imap_hostname.value.activate.connect(on_activated); - this.imap_tls.hide(); - this.imap_login.validator.state_changed.connect(on_validated); - this.imap_login.value.activate.connect(on_activated); - this.imap_password.validator.state_changed.connect(on_validated); - this.imap_password.value.activate.connect(on_activated); - - this.smtp_hostname.validator.state_changed.connect(on_validated); - this.smtp_hostname.value.activate.connect(on_activated); - this.smtp_tls.hide(); - this.smtp_auth.value.changed.connect(on_smtp_auth_changed); - this.smtp_login.validator.state_changed.connect(on_validated); - this.smtp_login.value.activate.connect(on_activated); - this.smtp_password.validator.state_changed.connect(on_validated); - this.smtp_password.value.activate.connect(on_activated); - - this.receiving_list.add(this.imap_hostname); - this.receiving_list.add(this.imap_tls); - this.receiving_list.add(this.imap_login); - this.receiving_list.add(this.imap_password); - - this.sending_list.add(this.smtp_hostname); - this.sending_list.add(this.smtp_tls); - this.sending_list.add(this.smtp_auth); - } - - internal Gtk.HeaderBar get_header() { - return this.header; + // XXX we need to make sure the validators for the service are wired up too + // this.smtp_auth.value.changed.connect(on_smtp_auth_changed); } private async void validate_account(GLib.Cancellable? cancellable) { @@ -140,18 +93,18 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { yield this.accounts.new_orphan_account( this.provider, new Geary.RFC822.MailboxAddress( - this.real_name.value.text.strip(), - this.email.value.text.strip() + this.name_row.text.strip(), + this.email_row.text.strip() ), cancellable ); - account.incoming = new_imap_service(); - account.outgoing = new_smtp_service(); + account.incoming = this.receiving_service_widget.service_mutable; + account.outgoing = this.sending_service_widget.service_mutable; account.untrusted_host.connect(on_untrusted_host); if (this.provider == Geary.ServiceProvider.OTHER && - this.imap_hostname.get_visible()) { + !this.did_auto_config) { bool imap_valid = false; bool smtp_valid = false; @@ -162,7 +115,7 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { imap_valid = true; } catch (Geary.ImapError.UNAUTHENTICATED err) { debug("Error authenticating IMAP service: %s", err.message); - to_focus = this.imap_login.value; + to_focus = this.receiving_service_widget; // Translators: In-app notification label message = _("Check your receiving login and password"); } catch (GLib.TlsError.BAD_CERTIFICATE err) { @@ -176,8 +129,9 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { Geary.ErrorContext context = new Geary.ErrorContext(err); debug("Error validating IMAP service: %s", context.format_full_error()); - this.imap_tls.show(); - to_focus = this.imap_hostname.value; + //XXX GTK4 not sure how to design a nice API for this + // this.imap_tls.show(); + to_focus = this.receiving_service_widget; // Translators: In-app notification label message = _("Check your receiving server details"); } @@ -197,9 +151,9 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { // There was an SMTP auth error, but IMAP already // succeeded, so the user probably needs to // specify custom creds here - this.smtp_auth.value.source = + this.receiving_service_widget.service.credentials_requirement = Geary.Credentials.Requirement.CUSTOM; - to_focus = this.smtp_login.value; + to_focus = this.receiving_service_widget; // Translators: In-app notification label message = _("Check your sending login and password"); } catch (GLib.TlsError.BAD_CERTIFICATE err) { @@ -212,8 +166,7 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { Geary.ErrorContext context = new Geary.ErrorContext(err); debug("Error validating SMTP service: %s", context.format_full_error()); - this.smtp_tls.show(); - to_focus = this.smtp_hostname.value; + to_focus = this.sending_service_widget; // Translators: In-app notification label message = _("Check your sending server details"); } @@ -228,7 +181,7 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { is_valid = true; } catch (Geary.ImapError.UNAUTHENTICATED err) { debug("Error authenticating provider: %s", err.message); - to_focus = this.email.value; + to_focus = this.email_row; // Translators: In-app notification label message = _("Check your email address and password"); } catch (GLib.TlsError.BAD_CERTIFICATE err) { @@ -248,7 +201,7 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { if (is_valid) { try { yield this.accounts.create_account(account, cancellable); - this.editor.pop(); + this.editor.pop_pane(); } catch (GLib.Error err) { debug("Failed to create new local account: %s", err.message); is_valid = false; @@ -268,8 +221,8 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { to_focus.grab_focus(); } if (message != null) { - this.editor.add_notification( - new Components.InAppNotification( + this.editor.add_toast( + new Adw.Toast( // Translators: In-app notification label, the // string substitution is a more detailed reason. _("Account not created: %s").printf(message) @@ -280,63 +233,23 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { } private Geary.ServiceInformation new_imap_service() { - Geary.ServiceInformation service = new Geary.ServiceInformation( + var service = new Geary.ServiceInformation( Geary.Protocol.IMAP, this.provider ); - service.credentials = new Geary.Credentials( - Geary.Credentials.Method.PASSWORD, - this.imap_login.value.get_text().strip(), - this.imap_password.value.get_text().strip() + Geary.Credentials.Method.PASSWORD, "" ); - - Components.NetworkAddressValidator host = - (Components.NetworkAddressValidator) - this.imap_hostname.validator; - GLib.NetworkAddress address = host.validated_address; - service.host = address.hostname; - service.port = (uint16) address.port; - service.transport_security = this.imap_tls.value.method; - - if (service.port == 0) { - service.port = service.get_default_port(); - } - return service; } private Geary.ServiceInformation new_smtp_service() { - Geary.ServiceInformation service = new Geary.ServiceInformation( + return new Geary.ServiceInformation( Geary.Protocol.SMTP, this.provider ); - - service.credentials_requirement = this.smtp_auth.value.source; - if (service.credentials_requirement == - Geary.Credentials.Requirement.CUSTOM) { - service.credentials = new Geary.Credentials( - Geary.Credentials.Method.PASSWORD, - this.smtp_login.value.get_text().strip(), - this.smtp_password.value.get_text().strip() - ); - } - - Components.NetworkAddressValidator host = - (Components.NetworkAddressValidator) - this.smtp_hostname.validator; - GLib.NetworkAddress address = host.validated_address; - - service.host = address.hostname; - service.port = (uint16) address.port; - service.transport_security = this.smtp_tls.value.method; - - if (service.port == 0) { - service.port = service.get_default_port(); - } - - return service; } private void check_validation() { +#if 0 bool server_settings_visible = this.stack.get_visible_child_name() == "server_settings"; bool controls_valid = true; Gtk.ListBox[] list_boxes; @@ -348,22 +261,23 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { list_boxes = new Gtk.ListBox[] { this.details_list }; } foreach (Gtk.ListBox list_box in list_boxes) { - list_box.foreach((child) => { - AddPaneRow? validatable = child as AddPaneRow; - if (validatable != null && !validatable.validator.is_valid) { - controls_valid = false; - } - }); + for (int i = 0; true; i++) { + unowned var validatable = list_box.get_row_at_index(i) as AddPaneRow; + if (validatable == null) + break; + if (!validatable.validator.is_valid) { + controls_valid = false; + } + } } this.action_button.set_sensitive(controls_valid); this.controls_valid = controls_valid; +#endif } private void update_operation_ui(bool is_running) { this.action_spinner.visible = is_running; - this.action_spinner.active = is_running; this.action_button.sensitive = !is_running; - this.back_button.sensitive = !is_running; this.sensitive = !is_running; } @@ -371,36 +285,37 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { this.stack.set_visible_child_name("user_settings"); this.action_button.set_label(_("_Next")); this.action_button.set_sensitive(true); - this.action_button.get_style_context().remove_class("suggested-action"); + this.action_button.remove_css_class("suggested-action"); } private void switch_to_server_settings() { this.stack.set_visible_child_name("server_settings"); this.action_button.set_label(_("_Create")); this.action_button.set_sensitive(false); - this.action_button.get_style_context().add_class("suggested-action"); + this.action_button.add_css_class("suggested-action"); } private void set_server_settings_from_autoconfig(AutoConfig auto_config, GLib.AsyncResult res) throws Accounts.AutoConfigError { AutoConfigValues auto_config_values = auto_config.get_config.end(res); - Gtk.Entry imap_hostname_entry = this.imap_hostname.value; - Gtk.Entry smtp_hostname_entry = this.smtp_hostname.value; - TlsComboBox imap_tls_combo_box = this.imap_tls.value; - TlsComboBox smtp_tls_combo_box = this.smtp_tls.value; - imap_hostname_entry.text = auto_config_values.imap_server + - ":" + auto_config_values.imap_port; - smtp_hostname_entry.text = auto_config_values.smtp_server + - ":" + auto_config_values.smtp_port; - imap_tls_combo_box.method = auto_config_values.imap_tls_method; - smtp_tls_combo_box.method = auto_config_values.smtp_tls_method; + Geary.ServiceInformation imap_service = this.receiving_service_widget.service; + Geary.ServiceInformation smtp_service = this.sending_service_widget.service; - this.imap_hostname.hide(); - this.smtp_hostname.hide(); - this.imap_tls.hide(); - this.smtp_tls.hide(); + imap_service.host = auto_config_values.imap_server; + imap_service.port = (uint16) uint.parse(auto_config_values.imap_port); + imap_service.transport_security = auto_config_values.imap_tls_method; + + smtp_service.host = auto_config_values.smtp_server; + smtp_service.port = (uint16) uint.parse(auto_config_values.smtp_port); + smtp_service.transport_security = auto_config_values.smtp_tls_method; + + //XXX GTK4 hide servr rows + // this.imap_hostname.hide(); + // this.smtp_hostname.hide(); + // this.imap_tls.hide(); + // this.smtp_tls.hide(); switch (auto_config_values.id) { case "googlemail.com": @@ -416,25 +331,26 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { } private void set_server_settings_from_hostname(string hostname) { - Gtk.Entry imap_hostname_entry = this.imap_hostname.value; - Gtk.Entry smtp_hostname_entry = this.smtp_hostname.value; + Geary.ServiceInformation imap_service = this.receiving_service_widget.service; + Geary.ServiceInformation smtp_service = this.sending_service_widget.service; string smtp_hostname = "smtp." + hostname; string imap_hostname = "imap." + hostname; string last_imap_hostname = ""; string last_smtp_hostname = ""; - this.imap_hostname.show(); - this.smtp_hostname.show(); + // XXX GTK4 show these again if an autoconf happened + // this.imap_hostname.show(); + // this.smtp_hostname.show(); if (this.last_valid_hostname != "") { last_imap_hostname = "imap." + this.last_valid_hostname; last_smtp_hostname = "smtp." + this.last_valid_hostname; } - if (imap_hostname_entry.text == last_imap_hostname) { - imap_hostname_entry.text = imap_hostname; + if (imap_service.host == last_imap_hostname) { + imap_service.host = imap_hostname; } - if (smtp_hostname_entry.text == last_smtp_hostname) { - smtp_hostname_entry.text = smtp_hostname; + if (smtp_service.host == last_smtp_hostname) { + smtp_service.host = smtp_hostname; } this.last_valid_hostname = hostname; } @@ -458,51 +374,59 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { if (add_local) { switch_to_server_settings(); } else { - this.editor.pop(); + this.editor.pop_pane(); } } ); } - private void on_validated(Components.Validator.Trigger reason) { + [GtkCallback] + private void on_validated(Components.ValidatorGroup validators, + Components.Validator validator) { check_validation(); - if (this.controls_valid && reason == Components.Validator.Trigger.ACTIVATED) { - this.action_button.clicked(); - } + //XXX GTK4 we somehow lost the Validator.Trigger here + // if (this.controls_valid && reason == Components.Validator.Trigger.ACTIVATED) { + // this.action_button.clicked(); + // } } + [GtkCallback] private void on_activated() { if (this.controls_valid) { this.action_button.clicked(); } } - private void on_email_changed() { - Gtk.Entry imap_login_entry = this.imap_login.value; - Gtk.Entry smtp_login_entry = this.smtp_login.value; + [GtkCallback] + private void on_email_row_changed(Gtk.Editable editable) { + var imap_service = this.receiving_service_widget.service; + var smtp_service = this.sending_service_widget.service; this.auto_config_cancellable.cancel(); - if (this.email.validator.state != Components.Validator.Validity.VALID) { + if (this.email_validator.state != Components.Validator.Validity.VALID) { return; } - string email = this.email.value.text; + string email = this.email_row.text; string hostname = email.split("@")[1]; // Do not update entries if changed by user - if (imap_login_entry.text == this.last_valid_email) { - imap_login_entry.text = email; + if (imap_service.credentials.user == this.last_valid_email) { + imap_service.credentials = new Geary.Credentials( + Geary.Credentials.Method.PASSWORD, email + ); } - if (smtp_login_entry.text == this.last_valid_email) { - smtp_login_entry.text = email; + if (smtp_service.credentials.user == this.last_valid_email) { + smtp_service.credentials = new Geary.Credentials( + Geary.Credentials.Method.PASSWORD, email + ); } this.last_valid_email = email; // Try to get configuration from Thunderbird autoconfig service this.action_spinner.visible = true; - this.action_spinner.active = true; this.auto_config_cancellable = new GLib.Cancellable(); var auto_config = new AutoConfig(this.auto_config_cancellable); auto_config.get_config.begin(hostname, (obj, res) => { @@ -513,19 +437,20 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { set_server_settings_from_hostname(hostname); } this.action_spinner.visible = false; - this.action_spinner.active = false; }); } private void on_smtp_auth_changed() { +#if 0 if (this.smtp_auth.value.source == Geary.Credentials.Requirement.CUSTOM) { - this.sending_list.add(this.smtp_login); - this.sending_list.add(this.smtp_password); + this.sending_list.append(this.smtp_login); + this.sending_list.append(this.smtp_password); } else if (this.smtp_login.parent != null) { this.sending_list.remove(this.smtp_login); this.sending_list.remove(this.smtp_password); } check_validation(); +#endif } private void on_untrusted_host(Geary.AccountInformation account, @@ -564,221 +489,4 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { this.validate_account.begin(this.op_cancellable); } } - - [GtkCallback] - private void on_back_button_clicked() { - if (this.stack.get_visible_child_name() == "user_settings") { - this.editor.pop(); - } else { - switch_to_user_settings(); - } - } - - [GtkCallback] - private bool on_list_keynav_failed(Gtk.Widget widget, - Gtk.DirectionType direction) { - bool ret = Gdk.EVENT_PROPAGATE; - Gtk.Container? next = null; - if (direction == Gtk.DirectionType.DOWN) { - if (widget == this.details_list) { - debug("Have details!"); - next = this.receiving_list; - } else if (widget == this.receiving_list) { - next = this.sending_list; - } - } else if (direction == Gtk.DirectionType.UP) { - if (widget == this.sending_list) { - next = this.receiving_list; - } else if (widget == this.receiving_list) { - next = this.details_list; - } - } - - if (next != null) { - next.child_focus(direction); - ret = Gdk.EVENT_STOP; - } - return ret; - } - -} - - -private abstract class Accounts.AddPaneRow : - LabelledEditorRow { - - - internal Components.Validator? validator { get; protected set; } - - - protected AddPaneRow(string label, Value value) { - base(label, value); - this.activatable = false; - } - -} - - -private abstract class Accounts.EntryRow : AddPaneRow { - - - private Components.EntryUndo undo; - - - protected EntryRow(string label, - string? initial_value = null, - string? placeholder = null) { - base(label, new Gtk.Entry()); - - this.value.text = initial_value ?? ""; - this.value.placeholder_text = placeholder ?? ""; - this.value.width_chars = 16; - - this.undo = new Components.EntryUndo(this.value); - } - - public override bool focus(Gtk.DirectionType direction) { - bool ret = Gdk.EVENT_PROPAGATE; - switch (direction) { - case Gtk.DirectionType.TAB_FORWARD: - case Gtk.DirectionType.TAB_BACKWARD: - ret = this.value.child_focus(direction); - break; - - default: - ret = base.focus(direction); - break; - } - - return ret; - } - -} - - -private class Accounts.NameRow : EntryRow { - - public NameRow(string default_name) { - // Translators: Label for the person's actual name when adding - // an account - base(_("Your name"), default_name.strip()); - this.validator = new Components.Validator(this.value); - if (this.value.text != "") { - // Validate if the string is non-empty so it will be good - // to go from the start - this.validator.validate(); - } - } - -} - - -private class Accounts.EmailRow : EntryRow { - - - public EmailRow() { - base( - _("Email address"), - null, - // Translators: Placeholder for the default sender address - // when adding an account - _("person@example.com") - ); - this.value.input_purpose = Gtk.InputPurpose.EMAIL; - this.validator = new Components.EmailValidator(this.value); - } - -} - - -private class Accounts.LoginRow : EntryRow { - - public LoginRow() { - // Translators: Label for an IMAP/SMTP service login/user name - // when adding an account - base(_("Login name")); - // Logins are not infrequently the same as the user's email - // address - this.value.input_purpose = Gtk.InputPurpose.EMAIL; - this.validator = new Components.Validator(this.value); - } - -} - - -private class Accounts.PasswordRow : EntryRow { - - - public PasswordRow() { - base(_("Password")); - this.value.visibility = false; - this.value.input_purpose = Gtk.InputPurpose.PASSWORD; - this.validator = new Components.Validator(this.value); - } - -} - - -private class Accounts.HostnameRow : EntryRow { - - - private Geary.Protocol type; - - - public HostnameRow(Geary.Protocol type) { - string label = ""; - string placeholder = ""; - switch (type) { - case Geary.Protocol.IMAP: - // Translators: Label for the IMAP server hostname when - // adding an account. - label = _("IMAP server"); - // Translators: Placeholder for the IMAP server hostname - // when adding an account. - placeholder = _("imap.example.com"); - break; - - case Geary.Protocol.SMTP: - // Translators: Label for the SMTP server hostname when - // adding an account. - label = _("SMTP server"); - // Translators: Placeholder for the SMTP server hostname - // when adding an account. - placeholder = _("smtp.example.com"); - break; - } - - base(label, null, placeholder); - this.type = type; - - this.validator = new Components.NetworkAddressValidator(this.value, 0); - } - -} - - -private class Accounts.TransportSecurityRow : - LabelledEditorRow { - - public TransportSecurityRow() { - TlsComboBox value = new TlsComboBox(); - base(value.label, value); - // Set to Transport TLS by default per RFC 8314 - this.value.method = Geary.TlsNegotiationMethod.TRANSPORT; - } - -} - - -private class Accounts.OutgoingAuthRow : - LabelledEditorRow { - - public OutgoingAuthRow() { - OutgoingAuthComboBox value = new OutgoingAuthComboBox(); - base(value.label, value); - - this.activatable = false; - this.value.source = Geary.Credentials.Requirement.USE_INCOMING; - } - } diff --git a/src/client/accounts/accounts-editor-edit-pane.vala b/src/client/accounts/accounts-editor-edit-pane.vala index 66aeb710..7397b67e 100644 --- a/src/client/accounts/accounts-editor-edit-pane.vala +++ b/src/client/accounts/accounts-editor-edit-pane.vala @@ -9,15 +9,9 @@ * An account editor pane for editing a specific account's preferences. */ [GtkTemplate (ui = "/org/gnome/Geary/accounts_editor_edit_pane.ui")] -internal class Accounts.EditorEditPane : - Gtk.Grid, EditorPane, AccountPane, CommandPane { +internal class Accounts.EditorEditPane : EditorPane, AccountPane, CommandPane { - /** {@inheritDoc} */ - internal Gtk.Widget initial_widget { - get { return this.details_list.get_row_at_index(0); } - } - /** {@inheritDoc} */ internal Geary.AccountInformation account { get ; protected set; } @@ -27,32 +21,28 @@ internal class Accounts.EditorEditPane : } /** {@inheritDoc} */ - internal bool is_operation_running { get; protected set; default = false; } + internal override bool is_operation_running { get; protected set; default = false; } /** {@inheritDoc} */ - internal GLib.Cancellable? op_cancellable { + internal override Cancellable? op_cancellable { get; protected set; default = null; } /** {@inheritDoc} */ - protected weak Accounts.Editor editor { get; set; } + protected override weak Accounts.Editor editor { get; set; } - [GtkChild] private unowned Gtk.HeaderBar header; + [GtkChild] private unowned Adw.HeaderBar header; - [GtkChild] private unowned Gtk.Grid pane_content; - - [GtkChild] private unowned Gtk.Adjustment pane_adjustment; - - [GtkChild] private unowned Gtk.ListBox details_list; + [GtkChild] private unowned Adw.EntryRow display_name_row; [GtkChild] private unowned Gtk.ListBox senders_list; - [GtkChild] private unowned Gtk.Frame signature_frame; + [GtkChild] private unowned Adw.PreferencesGroup signature_bin; private SignatureWebView signature_preview; private bool signature_changed = false; - [GtkChild] private unowned Gtk.ListBox settings_list; + [GtkChild] private unowned Adw.ComboRow email_prefetch_row; [GtkChild] private unowned Gtk.Button undo_button; @@ -63,24 +53,15 @@ internal class Accounts.EditorEditPane : this.editor = editor; this.account = account; - this.pane_content.set_focus_vadjustment(this.pane_adjustment); + update_display_name(); - this.details_list.set_header_func(Editor.seperator_headers); - this.details_list.add( - new DisplayNameRow(account, this.commands, this.op_cancellable) - ); - - this.senders_list.set_header_func(Editor.seperator_headers); foreach (Geary.RFC822.MailboxAddress sender in account.sender_mailboxes) { - this.senders_list.add(new_mailbox_row(sender)); + this.senders_list.append(new_mailbox_row(sender)); } - this.senders_list.add(new AddMailboxRow()); this.signature_preview = new SignatureWebView(editor.application.config); - this.signature_preview.events = ( - this.signature_preview.events | Gdk.EventType.FOCUS_CHANGE - ); + this.signature_preview.add_css_class("card"); this.signature_preview.content_loaded.connect(() => { // Only enable editability after the content has fully // loaded to avoid the WebProcess crashing. @@ -91,33 +72,30 @@ internal class Accounts.EditorEditPane : this.signature_preview.document_modified.connect(() => { this.signature_changed = true; }); - this.signature_preview.focus_out_event.connect(() => { - // This event will also be fired if the top-level - // window loses focus, e.g. if the user alt-tabs away, - // so don't execute the command if the signature web - // view no longer the focus widget - if (!this.signature_preview.is_focus && - this.signature_changed) { - this.commands.execute.begin( - new SignatureChangedCommand( - this.signature_preview, account - ), - this.op_cancellable - ); - } - return Gdk.EVENT_PROPAGATE; - }); + var focus_controller = new Gtk.EventControllerFocus(); + focus_controller.leave.connect(() => { + // This event will also be fired if the top-level + // window loses focus, e.g. if the user alt-tabs away, + // so don't execute the command if the signature web + // view no longer the focus widget + if (!this.signature_preview.is_focus() && + this.signature_changed) { + this.commands.execute.begin( + new SignatureChangedCommand( + this.signature_preview, account + ), + this.op_cancellable + ); + } + }); + this.signature_preview.add_controller(focus_controller); + + this.signature_bin.add(this.signature_preview); - this.signature_preview.show(); this.signature_preview.load_html( Geary.HTML.smart_escape(account.signature) ); - this.signature_frame.add(this.signature_preview); - - this.settings_list.set_header_func(Editor.seperator_headers); - this.settings_list.add(new EmailPrefetchRow(this)); - this.remove_button.set_visible( !this.editor.accounts.is_goa_account(account) ); @@ -131,8 +109,12 @@ internal class Accounts.EditorEditPane : disconnect_command_signals(); } + private void update_display_name() { + this.display_name_row.text = this.account.display_name; + } + internal string? get_default_name() { - string? name = account.primary_mailbox.name; + string? name = this.account.primary_mailbox.name; if (Geary.String.is_empty_or_whitespace(name)) { name = this.editor.accounts.get_account_name(); @@ -141,15 +123,11 @@ internal class Accounts.EditorEditPane : return name; } - /** {@inheritDoc} */ - internal Gtk.HeaderBar get_header() { - return this.header; - } - internal MailboxRow new_mailbox_row(Geary.RFC822.MailboxAddress sender) { - MailboxRow row = new MailboxRow(this.account, sender); - row.move_to.connect(on_sender_row_moved); - row.dropped.connect(on_sender_row_dropped); + MailboxRow row = new MailboxRow(this.account, sender, this); + //XXX GTK4 + // row.move_to.connect(on_sender_row_moved); + // row.dropped.connect(on_sender_row_dropped); return row; } @@ -192,85 +170,96 @@ internal class Accounts.EditorEditPane : ); } - [GtkCallback] - private void on_setting_activated(Gtk.ListBoxRow row) { - EditorRow? setting = row as EditorRow; - if (setting != null) { - setting.activated(this); - } - } - [GtkCallback] private void on_server_settings_clicked() { - this.editor.push(new EditorServersPane(this.editor, this.account)); + this.editor.push_pane(new EditorServersPane(this.editor, this.account)); } [GtkCallback] private void on_remove_account_clicked() { if (!this.editor.accounts.is_goa_account(account)) { - var button = new Gtk.Button.with_mnemonic(_("Remove Account")); - button.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - button.show(); + var dialog = new Adw.AlertDialog( + _("Remove Account: %s").printf(account.primary_mailbox.address), + _("This will remove it from Geary and delete locally cached email data from your computer. Nothing will be deleted from your service provider.") + ); + dialog.add_css_class("warning"); - var dialog = new Gtk.MessageDialog(this.editor, - Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, - Gtk.MessageType.WARNING, - Gtk.ButtonsType.NONE, - _("Remove Account: %s"), - account.primary_mailbox.address); - dialog.secondary_text = _("This will remove it from Geary and delete locally cached email data from your computer. Nothing will be deleted from your service provider."); + dialog.add_response("cancel", _("_Cancel")); + dialog.close_response = "cancel"; + dialog.add_response("remove", _("_Remove Account")); + dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE); - dialog.add_button (_("_Cancel"), Gtk.ResponseType.CANCEL); - dialog.add_action_widget(button, Gtk.ResponseType.ACCEPT); - - dialog.response.connect((response_id) => { - if (response_id == Gtk.ResponseType.ACCEPT) + dialog.choose.begin(this, null, (obj, res) => { + string response = dialog.choose.end(res); + if (response == "remove") this.editor.remove_account(this.account); - - dialog.destroy(); }); - dialog.show(); } } [GtkCallback] - private void on_back_button_clicked() { - this.editor.pop(); + private void on_add_mailbox_clicked(Gtk.Button add_button) { + var dialog = new MailboxEditorDialog.for_new(get_default_name()); + dialog.apply.connect((dialog, mailbox) => { + this.commands.execute.begin( + new AppendMailboxCommand( + this.senders_list, + new_mailbox_row(mailbox) + ), + this.op_cancellable + ); + dialog.close(); + }); + + dialog.present(this); } [GtkCallback] - private bool on_list_keynav_failed(Gtk.Widget widget, - Gtk.DirectionType direction) { - bool ret = Gdk.EVENT_PROPAGATE; - Gtk.Container? next = null; - if (direction == Gtk.DirectionType.DOWN) { - if (widget == this.details_list) { - next = this.senders_list; - } else if (widget == this.senders_list) { - this.signature_preview.grab_focus(); - } else if (widget == this.signature_preview) { - next = this.settings_list; - } - } else if (direction == Gtk.DirectionType.UP) { - if (widget == this.settings_list) { - this.signature_preview.grab_focus(); - } else if (widget == this.signature_preview) { - next = this.senders_list; - } else if (widget == this.senders_list) { - next = this.details_list; - } - } - - if (next != null) { - next.child_focus(direction); - ret = Gdk.EVENT_STOP; - } - return ret; + private static string period_to_string(Adw.EnumListItem item, + Accounts.PrefetchPeriod period) { + return period.to_string(); } - } +/** + * An enum to describe the possible values for the "Download Mail" option + */ +public enum Accounts.PrefetchPeriod { + + 2_WEEKS = 14, + 1_MONTH = 30, + 3_MONTHS = 90, + 6_MONTHS = 180, + 1_YEAR = 365, + 2_YEARS = 720, + 4_YEARS = 1461, + EVERYTHING = -1; + + public unowned string to_string() { + switch (this) { + case 2_WEEKS: + return _("2 weeks back"); + case 1_MONTH: + return _("1 month back"); + case 3_MONTHS: + return _("3 months back"); + case 6_MONTHS: + return _("6 months back"); + case 1_YEAR: + return _("1 year back"); + case 2_YEARS: + return _("2 years back"); + case 4_YEARS: + return _("4 years back"); + case EVERYTHING: + return _("Everything"); + } + + return_val_if_reached(""); + } +} + private class Accounts.DisplayNameRow : AccountRow { @@ -299,7 +288,9 @@ private class Accounts.DisplayNameRow : AccountRow { // undoable this.value_undo = new Components.EntryUndo(this.value); - this.value.focus_out_event.connect(on_focus_out); + var focus_controller = new Gtk.EventControllerFocus(); + focus_controller.leave.connect(on_focus_out); + this.value.add_controller(focus_controller); } public override void update() { @@ -337,231 +328,75 @@ private class Accounts.DisplayNameRow : AccountRow { } } - private bool on_focus_out() { + private void on_focus_out() { commit(); - return Gdk.EVENT_PROPAGATE; } } -private class Accounts.AddMailboxRow : AddRow { +private class Accounts.MailboxRow : Adw.ActionRow { + public Geary.AccountInformation account { get; construct set; } - public AddMailboxRow() { - // Translators: Tooltip for adding a new email sender/from - // address's address to an account - this.set_tooltip_text(_("Add a new sender email address")); - } + public Geary.RFC822.MailboxAddress mailbox { get; construct set; } - public override void activated(EditorEditPane pane) { - MailboxEditorPopover popover = new MailboxEditorPopover( - pane.get_default_name() ?? "", "", false - ); - popover.activated.connect(() => { - pane.commands.execute.begin( - new AppendMailboxCommand( - (Gtk.ListBox) get_parent(), - pane.new_mailbox_row( - new Geary.RFC822.MailboxAddress( - popover.display_name, - popover.address - ) - ) - ), - pane.op_cancellable - ); - popover.popdown(); - }); - - popover.set_relative_to(this); - popover.popup(); - } -} - - -private class Accounts.MailboxRow : AccountRow { - - - internal Geary.RFC822.MailboxAddress mailbox; + public unowned Accounts.EditorEditPane pane { get; construct set; } public MailboxRow(Geary.AccountInformation account, - Geary.RFC822.MailboxAddress mailbox) { - var label = new Gtk.Label(""); - label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); - label.set_line_wrap(true); - base(account, "", label); - this.mailbox = mailbox; - enable_drag(); + Geary.RFC822.MailboxAddress mailbox, + Accounts.EditorEditPane pane) { + Object( + account: account, + mailbox: mailbox, + pane: pane, + activatable: true + ); + //XXX GTK4 do this again + // enable_drag(); + + //XXX GTK4 also on notify update(); } - public override void activated(EditorEditPane pane) { - MailboxEditorPopover popover = new MailboxEditorPopover( - this.mailbox.name ?? "", - this.mailbox.address, + public override void activate() { + var dialog = new MailboxEditorDialog.for_existing( + this.mailbox, this.account.has_sender_aliases ); - popover.activated.connect(() => { - pane.commands.execute.begin( - new UpdateMailboxCommand( - this, - new Geary.RFC822.MailboxAddress( - popover.display_name, - popover.address - ) - ), - pane.op_cancellable - ); - popover.popdown(); - }); - popover.remove_clicked.connect(() => { - pane.commands.execute.begin( - new RemoveMailboxCommand(this), - pane.op_cancellable - ); - popover.popdown(); - }); - popover.set_relative_to(this); - popover.popup(); + dialog.apply.connect((dialog, mailbox) => { + this.pane.commands.execute.begin( + new UpdateMailboxCommand(this, mailbox), + this.pane.op_cancellable + ); + dialog.close(); + }); + + dialog.remove.connect((dialog) => { + this.pane.commands.execute.begin( + new RemoveMailboxCommand(this), + this.pane.op_cancellable + ); + dialog.close(); + }); + + dialog.present(this); } - public override void update() { + private void update() { + this.title = mailbox.address.strip(); + string? name = this.mailbox.name; if (Geary.String.is_empty_or_whitespace(name)) { // Translators: Label used to indicate the user has // provided no display name for one of their sender // email addresses in their account settings. name = _("Name not set"); - set_dim_label(true); - } else { - set_dim_label(false); - } - - this.label.set_text(name); - this.value.set_text(mailbox.address.strip()); - } - -} - -internal class Accounts.MailboxEditorPopover : EditorPopover { - - - public string display_name { get; private set; } - public string address { get; private set; } - - - private Gtk.Entry name_entry = new Gtk.Entry(); - private Components.EntryUndo name_undo; - private Gtk.Entry address_entry = new Gtk.Entry(); - private Components.EntryUndo address_undo; - private Components.EmailValidator address_validator; - private Gtk.Button remove_button; - - public signal void activated(); - public signal void remove_clicked(); - - - public MailboxEditorPopover(string? display_name, - string? address, - bool can_remove) { - this.display_name = display_name; - this.address = address; - - this.name_entry.set_text(display_name ?? ""); - this.name_entry.set_placeholder_text( - // Translators: This is used as a placeholder for the - // display name for an email address when editing a user's - // sender address preferences for an account. - _("Sender Name") - ); - this.name_entry.set_width_chars(20); - this.name_entry.changed.connect(on_name_changed); - this.name_entry.activate.connect(on_activate); - this.name_entry.show(); - - this.name_undo = new Components.EntryUndo(this.name_entry); - - this.address_entry.input_purpose = Gtk.InputPurpose.EMAIL; - this.address_entry.set_text(address ?? ""); - this.address_entry.set_placeholder_text( - // Translators: This is used as a placeholder for the - // address part of an email address when editing a user's - // sender address preferences for an account. - _("person@example.com") - ); - this.address_entry.set_width_chars(20); - this.address_entry.changed.connect(on_address_changed); - this.address_entry.activate.connect(on_activate); - this.address_entry.show(); - - this.address_undo = new Components.EntryUndo(this.address_entry); - - this.address_validator = - new Components.EmailValidator(this.address_entry); - - this.remove_button = new Gtk.Button.with_label(_("Remove")); - this.remove_button.halign = Gtk.Align.END; - this.remove_button.get_style_context().add_class( - "geary-setting-remove" - ); - this.remove_button.get_style_context().add_class( - Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION - ); - this.remove_button.clicked.connect(on_remove_clicked); - this.remove_button.show(); - - add_labelled_row( - // Translators: Label used for the display name part of an - // email address when editing a user's sender address - // preferences for an account. - _("Sender name"), - this.name_entry - ); - add_labelled_row( - // Translators: Label used for the address part of an - // email address when editing a user's sender address - // preferences for an account. - _("Email address"), - this.address_entry - ); - - if (can_remove) { - this.layout.attach(this.remove_button, 0, 2, 2, 1); - } - - this.popup_focus = this.name_entry; - } - - ~MailboxEditorPopover() { - this.name_entry.changed.disconnect(on_name_changed); - this.name_entry.activate.disconnect(on_activate); - - this.address_entry.changed.disconnect(on_address_changed); - this.address_entry.activate.disconnect(on_activate); - - this.remove_button.clicked.disconnect(on_remove_clicked); - } - - private void on_name_changed() { - this.display_name = this.name_entry.get_text().strip(); - } - - private void on_address_changed() { - this.address = this.address_entry.get_text().strip(); - } - - private void on_remove_clicked() { - remove_clicked(); - } - - private void on_activate() { - if (this.address_validator.state == Components.Validator.Validity.INDETERMINATE || this.address_validator.is_valid) { - activated(); } + this.subtitle = name; } } diff --git a/src/client/accounts/accounts-editor-list-pane.vala b/src/client/accounts/accounts-editor-list-pane.vala index 8cc55bd7..9dd2584f 100644 --- a/src/client/accounts/accounts-editor-list-pane.vala +++ b/src/client/accounts/accounts-editor-list-pane.vala @@ -9,7 +9,7 @@ * An account editor pane for listing all known accounts. */ [GtkTemplate (ui = "/org/gnome/Geary/accounts_editor_list_pane.ui")] -internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { +internal class Accounts.EditorListPane : Accounts.EditorPane, CommandPane { private static int ordinal_sort(Gtk.ListBoxRow a, Gtk.ListBoxRow b) { @@ -29,29 +29,22 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { /** {@inheritDoc} */ - internal Gtk.Widget initial_widget { - get { - return this.accounts_list; - } - } - - /** {@inheritDoc} */ - internal Application.CommandStack commands { + internal override Application.CommandStack commands { get; protected set; default = new Application.CommandStack(); } /** {@inheritDoc} */ - internal bool is_operation_running { get; protected set; default = false; } + internal override bool is_operation_running { get; protected set; default = false; } /** {@inheritDoc} */ - internal GLib.Cancellable? op_cancellable { + internal override Cancellable? op_cancellable { get; protected set; default = null; } internal Manager accounts { get; private set; } /** {@inheritDoc} */ - protected weak Accounts.Editor editor { get; set; } + protected override weak Accounts.Editor editor { get; set; } private bool show_welcome { get { @@ -59,11 +52,7 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { } } - [GtkChild] private unowned Gtk.HeaderBar header; - - [GtkChild] private unowned Gtk.Grid pane_content; - - [GtkChild] private unowned Gtk.Adjustment pane_adjustment; + [GtkChild] private unowned Adw.HeaderBar header; [GtkChild] private unowned Gtk.Grid welcome_panel; @@ -71,7 +60,7 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { [GtkChild] private unowned Gtk.ListBox accounts_list; - [GtkChild] private unowned Gtk.Frame accounts_list_frame; + [GtkChild] private unowned Gtk.ScrolledWindow accounts_list_scrolled; private Gee.Map edit_pane_cache = new Gee.HashMap(); @@ -85,9 +74,6 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { // without worrying about the editor's lifecycle this.accounts = editor.accounts; - this.pane_content.set_focus_vadjustment(this.pane_adjustment); - - this.accounts_list.set_header_func(Editor.seperator_headers); this.accounts_list.set_sort_func(ordinal_sort); foreach (Geary.AccountInformation account in this.accounts.iterable()) { add_account(account, this.accounts.get_status(account)); @@ -104,22 +90,22 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { update_welcome_panel(); } - public override void destroy() { + public override void dispose() { this.commands.executed.disconnect(on_execute); this.commands.undone.disconnect(on_undo); this.commands.redone.disconnect(on_execute); disconnect_command_signals(); this.accounts.account_added.disconnect(on_account_added); - this.accounts.account_status_changed.disconnect(on_account_status_changed); - this.accounts.account_removed.disconnect(on_account_removed); + this.accounts.account_status_changed.disconnect(on_account_status_changed); + this.accounts.account_removed.disconnect(on_account_removed); - this.edit_pane_cache.clear(); - base.destroy(); + this.edit_pane_cache.clear(); + base.dispose(); } internal void show_new_account() { - this.editor.push(new EditorAddPane(this.editor)); + this.editor.push_pane(new EditorAddPane(this.editor)); } internal void show_existing_account(Geary.AccountInformation account) { @@ -128,7 +114,7 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { edit_pane = new EditorEditPane(this.editor, account); this.edit_pane_cache.set(account, edit_pane); } - this.editor.push(edit_pane); + this.editor.push_pane(edit_pane); } /** Removes an account from the list. */ @@ -142,17 +128,12 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { } } - /** {@inheritDoc} */ - internal Gtk.HeaderBar get_header() { - return this.header; - } - private void add_account(Geary.AccountInformation account, Manager.Status status) { AccountListRow row = new AccountListRow(account, status); - row.move_to.connect(on_editor_row_moved); + row.moved.connect(on_account_row_moved); row.dropped.connect(on_editor_row_dropped); - this.accounts_list.add(row); + this.accounts_list.append(row); } private void update_welcome_panel() { @@ -160,24 +141,24 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { // No accounts are available, so show only the welcome // pane and service list. this.welcome_panel.show(); - this.accounts_list_frame.hide(); + this.accounts_list_scrolled.hide(); } else { // There are some accounts available, so show them and // the full add service UI. this.welcome_panel.hide(); - this.accounts_list_frame.show(); + this.accounts_list_scrolled.show(); } } private AccountListRow? get_account_row(Geary.AccountInformation account) { - AccountListRow? row = null; - this.accounts_list.foreach((child) => { - AccountListRow? account_row = child as AccountListRow; - if (account_row != null && account_row.account == account) { - row = account_row; - } - }); - return row; + for (int i = 0; true; i++) { + unowned var row = this.accounts_list.get_row_at_index(i) as AccountListRow; + if (row == null) + break; + if (row.account == account) + return row; + } + return null; } private void on_account_added(Geary.AccountInformation account, @@ -194,19 +175,17 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { } } - private void on_editor_row_moved(EditorRow source, int new_position) { + private void on_account_row_moved(AccountListRow source, int new_position) { this.commands.execute.begin( - new ReorderAccountCommand( - (AccountListRow) source, new_position, this.accounts - ), + new ReorderAccountCommand(source, new_position, this.accounts), this.op_cancellable ); } - private void on_editor_row_dropped(EditorRow source, EditorRow target) { + private void on_editor_row_dropped(AccountListRow source, AccountListRow target) { this.commands.execute.begin( new ReorderAccountCommand( - (AccountListRow) source, target.get_index(), this.accounts + source, target.get_index(), this.accounts ), this.op_cancellable ); @@ -222,34 +201,51 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { private void on_execute(Application.Command command) { if (command.executed_label != null) { - uint notification_time = - Components.InAppNotification.DEFAULT_DURATION; - if (command.executed_notification_brief) { - notification_time = - this.editor.application.config.brief_notification_duration; - } - Components.InAppNotification ian = new Components.InAppNotification( - command.executed_label, notification_time - ); - ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO)); - this.editor.add_notification(ian); + var toast = new Adw.Toast(command.executed_label); + toast.button_label = _("Undo"); + toast.action_name = Action.Edit.prefix(Action.Edit.UNDO); + if (command.executed_notification_brief) + toast.timeout = this.editor.application.config.brief_notification_duration; + this.editor.add_toast(toast); } } private void on_undo(Application.Command command) { if (command.undone_label != null) { - Components.InAppNotification ian = - new Components.InAppNotification(command.undone_label); - ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO)); - this.editor.add_notification(ian); + var toast = new Adw.Toast(command.undone_label); + toast.button_label = _("Redo"); + toast.action_name = Action.Edit.prefix(Action.Edit.REDO); + this.editor.add_toast(toast); } } [GtkCallback] private void on_row_activated(Gtk.ListBoxRow row) { - EditorRow? setting = row as EditorRow; - if (setting != null) { - setting.activated(this); + unowned var account_row = row as AccountListRow; + if (account_row == null) + return; + + Manager manager = this.accounts; + if (manager.is_goa_account(account_row.account) && + manager.get_status(account_row.account) != Manager.Status.ENABLED) { + // GOA account but it's disabled, so just take people + // directly to the GOA panel + manager.show_goa_account.begin( + account_row.account, this.op_cancellable, + (obj, res) => { + try { + manager.show_goa_account.end(res); + } catch (GLib.Error err) { + // XXX display an error to the user + debug( + "Failed to show GOA account \"%s\": %s", + account_row.account.id, + err.message + ); + } + }); + } else { + show_existing_account(account_row.account); } } @@ -260,28 +256,29 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane, CommandPane { } -private class Accounts.AccountListRow : AccountRow { +[GtkTemplate (ui = "/org/gnome/Geary/accounts-editor-account-list-row.ui")] +private class Accounts.AccountListRow : Adw.ActionRow { + public Geary.AccountInformation account { get; construct set; } - private Gtk.Label service_label = new Gtk.Label(""); - private Gtk.Image unavailable_icon = new Gtk.Image.from_icon_name( - "dialog-warning-symbolic", Gtk.IconSize.BUTTON - ); + [GtkChild] private unowned Gtk.Image drag_icon; + [GtkChild] private unowned Gtk.Image unavailable_icon; + + private bool drag_picked_up = false; + private double drag_x; + private double drag_y; + + public signal void moved(int new_position); + public signal void dropped(AccountListRow target); + + construct { + this.account.changed.connect(on_account_changed); + update(); + } public AccountListRow(Geary.AccountInformation account, Manager.Status status) { - base(account, "", new Gtk.Grid()); - enable_drag(); - - this.value.add(this.unavailable_icon); - this.value.add(this.service_label); - - this.service_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); - this.service_label.set_line_wrap(true); - this.service_label.show(); - - this.account.changed.connect(on_account_changed); - update(); + Object(account: account); update_status(status); } @@ -289,37 +286,12 @@ private class Accounts.AccountListRow : AccountRow { this.account.changed.disconnect(on_account_changed); } - public override void activated(EditorListPane pane) { - Manager manager = pane.accounts; - if (manager.is_goa_account(this.account) && - manager.get_status(this.account) != Manager.Status.ENABLED) { - // GOA account but it's disabled, so just take people - // directly to the GOA panel - manager.show_goa_account.begin( - account, pane.op_cancellable, - (obj, res) => { - try { - manager.show_goa_account.end(res); - } catch (GLib.Error err) { - // XXX display an error to the user - debug( - "Failed to show GOA account \"%s\": %s", - account.id, - err.message - ); - } - }); - } else { - pane.show_existing_account(this.account); - } - } - - public override void update() { + public void update() { string name = this.account.display_name; if (Geary.String.is_empty(name)) { name = account.primary_mailbox.to_address_display("", ""); } - this.label.set_text(name); + this.title = name; string? details = this.account.service_label; switch (account.service_provider) { @@ -335,32 +307,31 @@ private class Accounts.AccountListRow : AccountRow { // no-op: Use the generated label break; } - this.service_label.set_text(details); + this.subtitle = details; } public void update_status(Manager.Status status) { - bool enabled = false; switch (status) { case ENABLED: - enabled = true; - this.set_tooltip_text(""); + remove_css_class("dim-label"); + this.tooltip_text = ""; + this.unavailable_icon.visible = false; break; case DISABLED: - this.set_tooltip_text( - // Translators: Tooltip for accounts that have been - // loaded but disabled by the user. - _("This account has been disabled") - ); + // Translators: Tooltip for accounts that have been + // loaded but disabled by the user. + this.tooltip_text = _("This account has been disabled"); + add_css_class("dim-label"); + this.unavailable_icon.visible = true; break; case UNAVAILABLE: - this.set_tooltip_text( - // Translators: Tooltip for accounts that have been - // loaded but because of some error are not able to be - // used. - _("This account has encountered a problem and is unavailable") - ); + // Translators: Tooltip for accounts that have been loaded but + // because of some error are not able to be used. + this.tooltip_text = _("This account has encountered a problem and is unavailable"); + add_css_class("dim-label"); + this.unavailable_icon.visible = true; break; case REMOVED: @@ -368,23 +339,6 @@ private class Accounts.AccountListRow : AccountRow { break; } - this.unavailable_icon.set_visible(!enabled); - - if (enabled) { - this.label.get_style_context().remove_class( - Gtk.STYLE_CLASS_DIM_LABEL - ); - this.service_label.get_style_context().remove_class( - Gtk.STYLE_CLASS_DIM_LABEL - ); - } else { - this.label.get_style_context().add_class( - Gtk.STYLE_CLASS_DIM_LABEL - ); - this.service_label.get_style_context().add_class( - Gtk.STYLE_CLASS_DIM_LABEL - ); - } } private void on_account_changed() { @@ -395,6 +349,129 @@ private class Accounts.AccountListRow : AccountRow { } } + [GtkCallback] + private bool on_key_pressed(Gtk.EventControllerKey key_controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { + if (state != Gdk.ModifierType.CONTROL_MASK) + return Gdk.EVENT_PROPAGATE; + + int index = get_index(); + if (keyval == Gdk.Key.Up) { + index--; + if (index >= 0) { + moved(index); + return Gdk.EVENT_STOP; + } + } else if (keyval == Gdk.Key.Down) { + index++; + if (get_next_sibling() != null) { + moved(index); + return Gdk.EVENT_STOP; + } + } + + return Gdk.EVENT_PROPAGATE; + } + + // DND + + [GtkCallback] + private void on_drag_source_begin(Gtk.DragSource drag_source, + Gdk.Drag drag) { + + // Show our row while dragging + var drag_widget = new Gtk.ListBox(); + drag_widget.opacity = 0.8; + + Gtk.Allocation allocation; + get_allocation(out allocation); + drag_widget.set_size_request(allocation.width, allocation.height); + + var drag_row = new AccountListRow(this.account, Manager.Status.ENABLED); + drag_widget.append(drag_row); + drag_widget.drag_highlight_row(drag_row); + + var drag_icon = (Gtk.DragIcon) Gtk.DragIcon.get_for_drag(drag); + drag_icon.child = drag_widget; + drag.set_hotspot((int) this.drag_x, (int) this.drag_y); + + // Set a visual hint that the row is being dragged + add_css_class("geary-drag-source"); + this.drag_picked_up = true; + } + + [GtkCallback] + private void on_drag_source_end(Gtk.DragSource drag_source, + Gdk.Drag drag, + bool delete_data) { + remove_css_class("geary-drag-source"); + this.drag_picked_up = false; + } + + [GtkCallback] + private Gdk.ContentProvider on_drag_source_prepare(Gtk.DragSource drag_source, + double x, + double y) { + Graphene.Point p = { (float) x, (float) y }; + Graphene.Point p_row; + this.drag_icon.compute_point(this, p, out p_row); + this.drag_x = p_row.x; + this.drag_y = p_row.y; + + GLib.Value val = GLib.Value(typeof(int)); + val.set_int(get_index()); + return new Gdk.ContentProvider.for_value(val); + } + + [GtkCallback] + private Gdk.DragAction on_drop_target_enter(Gtk.DropTarget drop_target, + double x, + double y) { + // Don't highlight the same row that was picked up + if (!this.drag_picked_up) { + Gtk.ListBox? parent = get_parent() as Gtk.ListBox; + if (parent != null) { + parent.drag_highlight_row(this); + } + } + + return Gdk.DragAction.MOVE; + } + + [GtkCallback] + private void on_drop_target_leave(Gtk.DropTarget drop_target) { + if (!this.drag_picked_up) { + Gtk.ListBox? parent = get_parent() as Gtk.ListBox; + if (parent != null) { + parent.drag_unhighlight_row(); + } + } + } + + [GtkCallback] + private bool on_drop_target_drop(Gtk.DropTarget drop_target, + GLib.Value val, + double x, + double y) { + if (!val.holds(typeof(int))) { + warning("Can't deal with non-int row value"); + return false; + } + + int drag_index = val.get_int(); + Gtk.ListBox? parent = get_parent() as Gtk.ListBox; + if (parent != null) { + var drag_row = parent.get_row_at_index(drag_index) as AccountListRow; + if (drag_row != null && drag_row != this) { + drag_row.dropped(this); + return true; + } + } + + return false; + } } diff --git a/src/client/accounts/accounts-editor-row.vala b/src/client/accounts/accounts-editor-row.vala index d342ec3b..7a0e0f4f 100644 --- a/src/client/accounts/accounts-editor-row.vala +++ b/src/client/accounts/accounts-editor-row.vala @@ -8,20 +8,14 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { - private const string DND_ATOM = "geary-editor-row"; - private const Gtk.TargetEntry[] DRAG_ENTRIES = { - { DND_ATOM, Gtk.TargetFlags.SAME_APP, 0 } - }; - protected Gtk.Box layout { get; private set; default = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5); } - private Gtk.Container drag_handle; + private Gtk.Image drag_handle; private bool drag_picked_up = false; - private bool drag_entered = false; public signal void move_to(int new_position); @@ -29,66 +23,54 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { public EditorRow() { - - get_style_context().add_class("geary-settings"); - get_style_context().add_class("geary-labelled-row"); - - // We'd like to add the drag handle only when needed, but - // GNOME/gtk#1495 prevents us from doing so. - Gtk.EventBox drag_box = new Gtk.EventBox(); - drag_box.add( - new Gtk.Image.from_icon_name( - "list-drag-handle-symbolic", Gtk.IconSize.BUTTON - ) - ); - this.drag_handle = new Gtk.Grid(); - this.drag_handle.valign = Gtk.Align.CENTER; - this.drag_handle.add(drag_box); - this.drag_handle.show_all(); - this.drag_handle.hide(); - // Translators: Tooltip for dragging list items - this.drag_handle.set_tooltip_text(_("Drag to move this item")); + add_css_class("geary-settings"); + add_css_class("geary-labelled-row"); var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5); - box.add(drag_handle); - box.add(this.layout); - box.show(); - add(box); + this.child = box; - this.layout.show(); - this.show(); - this.size_allocate.connect((allocation) => { - if (allocation.width < 500) { - if (this.layout.orientation == Gtk.Orientation.HORIZONTAL) { - this.layout.orientation = Gtk.Orientation.VERTICAL; - } - } else if (this.layout.orientation == Gtk.Orientation.VERTICAL) { - this.layout.orientation = Gtk.Orientation.HORIZONTAL; - } - }); + var breakpoint_bin = new Adw.BreakpointBin(); + box.append(breakpoint_bin); + breakpoint_bin.child = this.layout; + + var breakpoint = new Adw.Breakpoint(Adw.BreakpointCondition.parse("max-width: 500px")); + breakpoint.add_setters(this.layout, "orientation", Gtk.Orientation.VERTICAL); + breakpoint_bin.add_breakpoint(breakpoint); + + this.drag_handle = new Gtk.Image.from_icon_name("list-drag-handle-symbolic"); + this.drag_handle.valign = Gtk.Align.CENTER; + this.drag_handle.visible = false; + // Translators: Tooltip for dragging list items + this.drag_handle.set_tooltip_text(_("Drag to move this item")); + box.append(this.drag_handle); + + var key_controller = new Gtk.EventControllerKey(); + key_controller.key_pressed.connect(on_key_pressed); + add_controller(key_controller); } public virtual void activated(PaneType pane) { // No-op by default } - public override bool key_press_event(Gdk.EventKey event) { + private bool on_key_pressed(Gtk.EventControllerKey key_controller, uint keyval, uint keycode, Gdk.ModifierType state) { bool ret = Gdk.EVENT_PROPAGATE; - if (event.state == Gdk.ModifierType.CONTROL_MASK) { + if (state == Gdk.ModifierType.CONTROL_MASK) { int index = get_index(); - if (event.keyval == Gdk.Key.Up) { + if (keyval == Gdk.Key.Up) { index -= 1; if (index >= 0) { move_to(index); ret = Gdk.EVENT_STOP; } - } else if (event.keyval == Gdk.Key.Down) { + } else if (keyval == Gdk.Key.Down) { index += 1; Gtk.ListBox? parent = get_parent() as Gtk.ListBox; if (parent != null && - index < parent.get_children().length() && + //XXX GTK4 - I *think* we don't need this anymore + // index < parent.get_children().length() && !(parent.get_row_at_index(index) is AddRow)) { move_to(index); ret = Gdk.EVENT_STOP; @@ -96,127 +78,113 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { } } - if (ret != Gdk.EVENT_STOP) { - ret = base.key_press_event(event); - } - return ret; } /** Adds a drag handle to the row and enables drag signals. */ protected void enable_drag() { - Gtk.drag_source_set( - this.drag_handle, - Gdk.ModifierType.BUTTON1_MASK, - DRAG_ENTRIES, - Gdk.DragAction.MOVE - ); + //XXX GTK4 - is this activated on click? + Gtk.DragSource drag_source = new Gtk.DragSource(); + drag_source.drag_begin.connect(on_drag_source_begin); + drag_source.drag_end.connect(on_drag_source_end); + drag_source.prepare.connect(on_drag_source_prepare); + this.drag_handle.add_controller(drag_source); - Gtk.drag_dest_set( - this, - // No highlight, we'll take care of that ourselves so we - // can avoid highlighting the row that was picked up - Gtk.DestDefaults.MOTION | Gtk.DestDefaults.DROP, - DRAG_ENTRIES, - Gdk.DragAction.MOVE - ); + Gtk.DropTarget drop_target = new Gtk.DropTarget(typeof(int), Gdk.DragAction.MOVE); + drop_target.enter.connect(on_drop_target_enter); + drop_target.leave.connect(on_drop_target_leave); + drop_target.drop.connect(on_drop_target_drop); + this.drag_handle.add_controller(drop_target); - this.drag_handle.drag_begin.connect(on_drag_begin); - this.drag_handle.drag_end.connect(on_drag_end); - this.drag_handle.drag_data_get.connect(on_drag_data_get); + //XXX GTK4 - Disable highlight by default, so we can avoid highlighting the row that was picked up + this.drag_handle.add_css_class("geary-drag-handle"); + this.drag_handle.visible = true; - this.drag_motion.connect(on_drag_motion); - this.drag_leave.connect(on_drag_leave); - this.drag_data_received.connect(on_drag_data_received); - - this.drag_handle.get_style_context().add_class("geary-drag-handle"); - this.drag_handle.show(); - - get_style_context().add_class("geary-draggable"); + add_css_class("geary-draggable"); } - private void on_drag_begin(Gdk.DragContext context) { + private void on_drag_source_begin(Gtk.DragSource drag_source, Gdk.Drag drag) { // Draw a nice drag icon Gtk.Allocation alloc = Gtk.Allocation(); this.get_allocation(out alloc); - Cairo.ImageSurface surface = new Cairo.ImageSurface( - Cairo.Format.ARGB32, alloc.width, alloc.height - ); - Cairo.Context paint = new Cairo.Context(surface); + //XXX GTK4 lol, let's just make this a proper drag icon at some point + // Cairo.ImageSurface surface = new Cairo.ImageSurface( + // Cairo.Format.ARGB32, alloc.width, alloc.height + // ); + // Cairo.Context paint = new Cairo.Context(surface); - Gtk.StyleContext style = get_style_context(); - style.add_class("geary-drag-icon"); - draw(paint); - style.remove_class("geary-drag-icon"); + // add_css_class("geary-drag-icon"); + // draw(paint); + // remove_css_class("geary-drag-icon"); - int x, y; - this.drag_handle.translate_coordinates(this, 0, 0, out x, out y); - surface.set_device_offset(-x, -y); - Gtk.drag_set_icon_surface(context, surface); + // drag_source.set_icon(surface, 0, 0); // Set a visual hint that the row is being dragged - style.add_class("geary-drag-source"); + add_css_class("geary-drag-source"); this.drag_picked_up = true; } - private void on_drag_end(Gdk.DragContext context) { - get_style_context().remove_class("geary-drag-source"); + private void on_drag_source_end(Gtk.DragSource drag_source, + Gdk.Drag drag, + bool delete_data) { + remove_css_class("geary-drag-source"); this.drag_picked_up = false; } - private bool on_drag_motion(Gdk.DragContext context, - int x, int y, - uint time_) { - if (!this.drag_entered) { - this.drag_entered = true; - - // Don't highlight the same row that was picked up - if (!this.drag_picked_up) { - Gtk.ListBox? parent = get_parent() as Gtk.ListBox; - if (parent != null) { - parent.drag_highlight_row(this); - } + private Gdk.DragAction on_drop_target_enter(Gtk.DropTarget drop_target, + double x, + double y) { + // Don't highlight the same row that was picked up + if (!this.drag_picked_up) { + Gtk.ListBox? parent = get_parent() as Gtk.ListBox; + if (parent != null) { + parent.drag_highlight_row(this); } } - return true; + return Gdk.DragAction.MOVE; } - private void on_drag_leave(Gdk.DragContext context, - uint time_) { + private void on_drop_target_leave(Gtk.DropTarget drop_target) { if (!this.drag_picked_up) { Gtk.ListBox? parent = get_parent() as Gtk.ListBox; if (parent != null) { parent.drag_unhighlight_row(); } } - this.drag_entered = false; } - private void on_drag_data_get(Gdk.DragContext context, - Gtk.SelectionData selection_data, - uint info, uint time_) { - selection_data.set( - Gdk.Atom.intern_static_string(DND_ATOM), 8, - get_index().to_string().data - ); + private Gdk.ContentProvider on_drag_source_prepare(Gtk.DragSource drag_source, + double x, + double y) { + GLib.Value val = GLib.Value(typeof(int)); + val.set_int(get_index()); + return new Gdk.ContentProvider.for_value(val); } - private void on_drag_data_received(Gdk.DragContext context, - int x, int y, - Gtk.SelectionData selection_data, - uint info, uint time_) { - int drag_index = int.parse((string) selection_data.get_data()); + private bool on_drop_target_drop(Gtk.DropTarget drop_target, + GLib.Value val, + double x, + double y) { + if (!val.holds(typeof(int))) { + warning("Can't deal with non-uint row value"); + return false; + } + + int drag_index = val.get_int(); Gtk.ListBox? parent = this.get_parent() as Gtk.ListBox; if (parent != null) { EditorRow? drag_row = parent.get_row_at_index(drag_index) as EditorRow; if (drag_row != null && drag_row != this) { drag_row.dropped(this); + return true; } } + + return false; } } @@ -233,11 +201,10 @@ internal class Accounts.LabelledEditorRow : EditorRow { this.label.halign = Gtk.Align.START; this.label.valign = Gtk.Align.CENTER; this.label.hexpand = true; - this.label.set_text(label); - this.label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); - this.label.set_line_wrap(true); - this.label.show(); - this.layout.add(this.label); + this.label.label = label; + this.label.wrap_mode = Pango.WrapMode.WORD_CHAR; + this.label.wrap = true; + this.layout.append(this.label); bool expand_label = true; this.value = value; @@ -250,14 +217,13 @@ internal class Accounts.LabelledEditorRow : EditorRow { } Gtk.Label? vlabel = value as Gtk.Label; if (vlabel != null) { - vlabel.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); - vlabel.set_line_wrap(true); + vlabel.wrap_mode = Pango.WrapMode.WORD_CHAR; + vlabel.wrap = true; } widget.halign = Gtk.Align.START; widget.valign = Gtk.Align.CENTER; - widget.show(); - this.layout.add(widget); + this.layout.append(widget); } this.label.hexpand = expand_label; @@ -265,9 +231,9 @@ internal class Accounts.LabelledEditorRow : EditorRow { public void set_dim_label(bool is_dim) { if (is_dim) { - this.label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.label.add_css_class("dim-label"); } else { - this.label.get_style_context().remove_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.label.remove_css_class("dim-label"); } } @@ -278,51 +244,11 @@ internal class Accounts.AddRow : EditorRow { public AddRow() { - get_style_context().add_class("geary-add-row"); - Gtk.Image add_icon = new Gtk.Image.from_icon_name( - "list-add-symbolic", Gtk.IconSize.BUTTON - ); + add_css_class("geary-add-row"); + var add_icon = new Gtk.Image.from_icon_name("list-add-symbolic"); add_icon.set_hexpand(true); - add_icon.show(); - this.layout.add(add_icon); - } - -} - - -internal class Accounts.ServiceProviderRow : - LabelledEditorRow { - - - public ServiceProviderRow(Geary.ServiceProvider provider, - string other_type_label) { - string? label = null; - switch (provider) { - case Geary.ServiceProvider.GMAIL: - label = _("Gmail"); - break; - - case Geary.ServiceProvider.OUTLOOK: - label = _("Outlook.com"); - break; - - case Geary.ServiceProvider.OTHER: - label = other_type_label; - break; - } - - base( - // Translators: Label describes the service provider - // hosting the email account, e.g. Gmail, Yahoo, or some - // other generic IMAP service. - _("Service provider"), - new Gtk.Label(label) - ); - - // Can't change this, so deactivate and dim out - set_activatable(false); - this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.layout.append(add_icon); } } @@ -390,7 +316,7 @@ private abstract class Accounts.ServiceRow : AccountRow Gtk.Widget? widget = value as Gtk.Widget; if (widget != null && !is_editable) { if (widget is Gtk.Label) { - widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + widget.add_css_class("dim-label"); } else { widget.set_sensitive(false); } @@ -533,7 +459,6 @@ internal class Accounts.TlsComboBox : Gtk.ComboBox { } - internal class Accounts.OutgoingAuthComboBox : Gtk.ComboBox { @@ -626,13 +551,12 @@ internal class Accounts.EditorPopover : Gtk.Popover { public EditorPopover() { - get_style_context().add_class("geary-editor"); + add_css_class("geary-editor"); this.layout.orientation = Gtk.Orientation.VERTICAL; this.layout.set_row_spacing(6); this.layout.set_column_spacing(12); - this.layout.show(); - add(this.layout); + this.child = this.layout; this.closed.connect_after(on_closed); } @@ -641,39 +565,12 @@ internal class Accounts.EditorPopover : Gtk.Popover { this.closed.disconnect(on_closed); } - /** {@inheritDoc} */ - public new void popup() { - // Work-around GTK+ issue #1138 - Gtk.Widget target = get_relative_to(); - - Gtk.Allocation content_area; - target.get_allocation(out content_area); - - Gtk.StyleContext style = target.get_style_context(); - Gtk.StateFlags flags = style.get_state(); - Gtk.Border margin = style.get_margin(flags); - - content_area.x = margin.left; - content_area.y = margin.bottom; - content_area.width -= (content_area.x + margin.right); - content_area.height -= (content_area.y + margin.top); - - set_pointing_to(content_area); - - base.popup(); - - if (this.popup_focus != null) { - this.popup_focus.grab_focus(); - } - } - public void add_labelled_row(string label, Gtk.Widget value) { Gtk.Label label_widget = new Gtk.Label(label); - label_widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + label_widget.add_css_class("dim-label"); label_widget.halign = Gtk.Align.END; - label_widget.show(); - this.layout.add(label_widget); + this.layout.attach_next_to(label_widget, null, Gtk.PositionType.BOTTOM); this.layout.attach_next_to(value, label_widget, Gtk.PositionType.RIGHT); } diff --git a/src/client/accounts/accounts-editor-servers-pane.vala b/src/client/accounts/accounts-editor-servers-pane.vala index 982acd5c..d7603657 100644 --- a/src/client/accounts/accounts-editor-servers-pane.vala +++ b/src/client/accounts/accounts-editor-servers-pane.vala @@ -10,12 +10,11 @@ * An account editor pane for editing server details for an account. */ [GtkTemplate (ui = "/org/gnome/Geary/accounts_editor_servers_pane.ui")] -internal class Accounts.EditorServersPane : - Gtk.Grid, EditorPane, AccountPane, CommandPane { +internal class Accounts.EditorServersPane : EditorPane, AccountPane, CommandPane { /** {@inheritDoc} */ - internal weak Accounts.Editor editor { get; set; } + internal override weak Accounts.Editor editor { get; set; } /** {@inheritDoc} */ internal Geary.AccountInformation account { get ; protected set; } @@ -26,200 +25,67 @@ internal class Accounts.EditorServersPane : } /** {@inheritDoc} */ - internal Gtk.Widget initial_widget { - get { return this.details_list; } - } - - /** {@inheritDoc} */ - internal bool is_operation_running { + internal override bool is_operation_running { get { return !this.sensitive; } protected set { update_operation_ui(value); } } /** {@inheritDoc} */ - internal GLib.Cancellable? op_cancellable { + internal override Cancellable? op_cancellable { get; protected set; default = new GLib.Cancellable(); } private Geary.Engine engine; - // These are copies of the originals that can be updated before - // validating on apply, without breaking anything. - private Geary.ServiceInformation incoming_mutable; - private Geary.ServiceInformation outgoing_mutable; - - private Gee.List validators = - new Gee.LinkedList(); + public Components.ValidatorGroup validators { get; construct set; } - [GtkChild] private unowned Gtk.HeaderBar header; + [GtkChild] private unowned Adw.ActionRow account_provider_row; + [GtkChild] private unowned Adw.ActionRow service_provider_row; + [GtkChild] private unowned Adw.SwitchRow save_drafts_row; + [GtkChild] private unowned Adw.SwitchRow save_sent_row; - [GtkChild] private unowned Gtk.Grid pane_content; - - [GtkChild] private unowned Gtk.Adjustment pane_adjustment; - - [GtkChild] private unowned Gtk.ListBox details_list; - - [GtkChild] private unowned Gtk.ListBox receiving_list; - - [GtkChild] private unowned Gtk.ListBox sending_list; + [GtkChild] private unowned ServiceInformationWidget receiving_service_widget; + [GtkChild] private unowned ServiceInformationWidget sending_service_widget; [GtkChild] private unowned Gtk.Button apply_button; - [GtkChild] private unowned Gtk.Spinner apply_spinner; - private SaveDraftsRow save_drafts; - private SaveSentRow save_sent; - private ServiceLoginRow incoming_login; - private ServicePasswordRow incoming_password; + static construct { + typeof(ServiceInformationWidget).ensure(); - private ServiceOutgoingAuthRow outgoing_auth; - private ServiceLoginRow outgoing_login; - private ServicePasswordRow outgoing_password; + install_action("apply", null, (Gtk.WidgetActionActivateFunc) action_apply); + } public EditorServersPane(Editor editor, Geary.AccountInformation account) { this.editor = editor; this.account = account; this.engine = editor.application.engine; - this.incoming_mutable = new Geary.ServiceInformation.copy(account.incoming); - this.outgoing_mutable = new Geary.ServiceInformation.copy(account.outgoing); - this.pane_content.set_focus_vadjustment(this.pane_adjustment); // Details + fill_in_account_provider(editor.accounts); + fill_in_service_provider(); - this.details_list.set_header_func(Editor.seperator_headers); - // Only add an account provider if it is esoteric enough. - if (this.account.mediator is GoaMediator) { - this.details_list.add( - new AccountProviderRow(editor.accounts, this.account) - ); - } - ServiceProviderRow service_provider = - new ServiceProviderRow( - this.account.service_provider, - this.account.service_label - ); - service_provider.set_dim_label(true); - service_provider.activatable = false; - add_row(this.details_list, service_provider); + this.receiving_service_widget.service = account.incoming; + this.sending_service_widget.service = account.outgoing; - this.save_drafts = new SaveDraftsRow( - this.account, this.commands, this.op_cancellable - ); - add_row(this.details_list, this.save_drafts); + bool services_editable = !(account.mediator is GoaMediator); + this.receiving_service_widget.set_editable(services_editable); + this.sending_service_widget.set_editable(services_editable); - this.save_sent = new SaveSentRow( - this.account, this.commands, this.op_cancellable - ); - switch (account.service_provider) { - case OTHER: - add_row(this.details_list, this.save_sent); - break; - default: - // XXX GMail and Outlook auto-save sent mail so don't - // include save sent option, but we shouldn't be - // hard-coding visible rows like this - break; - } + //XXX GTK4 Make sure we update save_drafts and save_sent - // Receiving - - this.receiving_list.set_header_func(Editor.seperator_headers); - add_row( - this.receiving_list, - new ServiceHostRow( - account, - this.incoming_mutable, - this.commands, - this.op_cancellable - ) - ); - add_row( - this.receiving_list, - new ServiceSecurityRow( - account, - this.incoming_mutable, - this.commands, - this.op_cancellable - ) - ); - - this.incoming_password = new ServicePasswordRow( - account, - this.incoming_mutable, - this.commands, - this.op_cancellable - ); - - this.incoming_login = new ServiceLoginRow( - account, - this.incoming_mutable, - this.commands, - this.op_cancellable, - this.incoming_password - ); - - add_row(this.receiving_list, this.incoming_login); - add_row(this.receiving_list, this.incoming_password); - - // Sending - - this.sending_list.set_header_func(Editor.seperator_headers); - add_row( - this.sending_list, - new ServiceHostRow( - account, - this.outgoing_mutable, - this.commands, - this.op_cancellable - ) - ); - add_row( - this.sending_list, - new ServiceSecurityRow( - account, - this.outgoing_mutable, - this.commands, - this.op_cancellable - ) - ); - this.outgoing_auth = new ServiceOutgoingAuthRow( - account, - this.outgoing_mutable, - this.incoming_mutable, - this.commands, - this.op_cancellable - ); - this.outgoing_auth.value.changed.connect(on_outgoing_auth_changed); - add_row(this.sending_list, this.outgoing_auth); - - this.outgoing_password = new ServicePasswordRow( - account, - this.outgoing_mutable, - this.commands, - this.op_cancellable - ); - - this.outgoing_login = new ServiceLoginRow( - account, - this.outgoing_mutable, - this.commands, - this.op_cancellable, - this.outgoing_password - ); - - add_row(this.sending_list, this.outgoing_login); - add_row(this.sending_list, this.outgoing_password); + // XXX GMail and Outlook auto-save sent mail so don't include save sent + // option, but we shouldn't be hard-coding visible rows like this + this.save_sent_row.visible = (account.service_provider == OTHER); // Misc plumbing connect_account_signals(); connect_command_signals(); - - update_outgoing_auth(); } ~EditorServersPane() { @@ -227,9 +93,48 @@ internal class Accounts.EditorServersPane : disconnect_command_signals(); } - /** {@inheritDoc} */ - internal Gtk.HeaderBar get_header() { - return this.header; + private void fill_in_account_provider(Manager accounts) { + if (this.account.mediator is GoaMediator) { + this.account_provider_row.subtitle = _("GNOME Online Accounts"); + + var button = new Gtk.Button.from_icon_name("external-link-symbolic"); + button.valign = Gtk.Align.CENTER; + button.clicked.connect((button) => { + if (accounts.is_goa_account(this.account)) { + accounts.show_goa_account.begin( + account, this.op_cancellable, + (obj, res) => { + try { + accounts.show_goa_account.end(res); + } catch (GLib.Error err) { + // XXX display an error to the user + debug( + "Failed to show GOA account \"%s\": %s", + account.id, + err.message + ); + } + }); + } + }); + this.account_provider_row.add_suffix(button); + } + } + + private void fill_in_service_provider() { + switch (this.account.service_provider) { + case Geary.ServiceProvider.GMAIL: + this.service_provider_row.subtitle = _("Gmail"); + break; + + case Geary.ServiceProvider.OUTLOOK: + this.service_provider_row.subtitle = _("Outlook.com"); + break; + + case Geary.ServiceProvider.OTHER: + this.service_provider_row.subtitle = this.account.service_label; + break; + } } /** {@inheritDoc} */ @@ -238,11 +143,8 @@ internal class Accounts.EditorServersPane : this.apply_button.set_sensitive(this.commands.can_undo); } - private bool is_valid() { - return Geary.traverse(this.validators).all((v) => v.is_valid); - } - private async void save(GLib.Cancellable? cancellable) { +#if 0 this.is_operation_running = true; // Only need to validate if a generic, local account since @@ -278,18 +180,19 @@ internal class Accounts.EditorServersPane : this.account.changed(); } - this.editor.pop(); + this.editor.pop_pane(); } else { // Re-enable apply so that the same config can be re-tried // in the face of transient errors, without having to // change something to re-enable it - this.apply_button.set_sensitive(true); + this.apply_button.sensitive = true; // Undo these manually since it would have been updated // already by the command this.account.save_drafts = this.save_drafts.initial_value; this.account.save_sent = this.save_sent.initial_value; } +#endif } private async bool validate(GLib.Cancellable? cancellable) { @@ -301,9 +204,12 @@ internal class Accounts.EditorServersPane : string? message = null; bool imap_valid = false; + try { yield this.engine.validate_imap( - local_account, this.incoming_mutable, cancellable + local_account, + this.receiving_service_widget.service_mutable, + cancellable ); imap_valid = true; } catch (Geary.ImapError.UNAUTHENTICATED err) { @@ -331,8 +237,8 @@ internal class Accounts.EditorServersPane : try { yield this.engine.validate_smtp( local_account, - this.outgoing_mutable, - this.incoming_mutable.credentials, + this.sending_service_widget.service_mutable, + this.receiving_service_widget.service_mutable.credentials, cancellable ); smtp_valid = true; @@ -341,7 +247,8 @@ internal class Accounts.EditorServersPane : // There was an SMTP auth error, but IMAP already // succeeded, so the user probably needs to // specify custom creds here - this.outgoing_auth.value.source = Geary.Credentials.Requirement.CUSTOM; + //XXX GTK4 + // this.outgoing_auth.value.source = Geary.Credentials.Requirement.CUSTOM; // Translators: In-app notification label message = _("Check your sending login and password"); } catch (GLib.TlsError.BAD_CERTIFICATE err) { @@ -366,8 +273,8 @@ internal class Accounts.EditorServersPane : debug("Validation complete, is valid: %s", is_valid.to_string()); if (!is_valid && message != null) { - this.editor.add_notification( - new Components.InAppNotification( + this.editor.add_toast( + new Adw.Toast( // Translators: In-app notification label, the // string substitution is a more detailed reason. _("Account not updated: %s").printf(message) @@ -381,6 +288,8 @@ internal class Accounts.EditorServersPane : private async bool update_service(Geary.ServiceInformation existing, Geary.ServiceInformation copy, GLib.Cancellable cancellable) { + return true; +#if 0 bool has_changed = !existing.equal_to(copy); if (has_changed) { try { @@ -410,40 +319,28 @@ internal class Accounts.EditorServersPane : } } return has_changed; - } - - private void add_row(Gtk.ListBox list, EditorRow row) { - list.add(row); - ValidatingRow? validating = row as ValidatingRow; - if (validating != null) { - validating.changed.connect(on_validator_changed); - validating.validator.activated.connect_after(on_validator_activated); - this.validators.add(validating.validator); - } - } - - private void update_outgoing_auth() { - this.outgoing_login.set_visible( - this.outgoing_auth.value.source == CUSTOM - ); +#endif } private void update_operation_ui(bool is_running) { this.apply_spinner.visible = is_running; - this.apply_spinner.active = is_running; this.apply_button.sensitive = !is_running; this.sensitive = !is_running; } - private void on_validator_changed() { - this.apply_button.set_sensitive(is_valid()); - } + // [GtkCallback] + // private void on_validators_changed(Components.ValidatorGroup validators, + // Components.Validator validator) { + // action_set_enabled("apply", validators.is_valid()); + // } - private void on_validator_activated() { - if (is_valid()) { - this.apply_button.clicked(); - } - } + // [GtkCallback] + // private void on_validators_activated(Components.ValidatorGroup validators, + // Components.Validator validator) { + // if (validators.is_valid()) { + // activate_action("apply", null); + // } + // } private void on_untrusted_host(Geary.AccountInformation account, Geary.ServiceInformation service, @@ -465,132 +362,39 @@ internal class Accounts.EditorServersPane : }); } + //XXX GTK4 we don't have a cancel button anymore +#if 0 [GtkCallback] private void on_cancel_button_clicked() { if (this.is_operation_running) { cancel_operation(); } else { - this.editor.pop(); + this.editor.pop_pane(); } } +#endif - [GtkCallback] - private void on_apply_button_clicked() { + private void action_apply(string action_name, Variant? param) { this.save.begin(this.op_cancellable); } - [GtkCallback] - private bool on_list_keynav_failed(Gtk.Widget widget, - Gtk.DirectionType direction) { - bool ret = Gdk.EVENT_PROPAGATE; - Gtk.Container? next = null; - if (direction == Gtk.DirectionType.DOWN) { - if (widget == this.details_list) { - next = this.receiving_list; - } else if (widget == this.receiving_list) { - next = this.sending_list; - } - } else if (direction == Gtk.DirectionType.UP) { - if (widget == this.sending_list) { - next = this.receiving_list; - } else if (widget == this.receiving_list) { - next = this.details_list; - } - } - - if (next != null) { - next.child_focus(direction); - ret = Gdk.EVENT_STOP; - } - return ret; - } - - private void on_outgoing_auth_changed() { - update_outgoing_auth(); - } - - [GtkCallback] - private void on_activate(Gtk.ListBoxRow row) { - Accounts.EditorRow server_row = - row as Accounts.EditorRow; - if (server_row != null) { - server_row.activated(this); - } - } +} +private struct Accounts.InitialConfiguration { + bool save_drafts; + bool save_sent; } -private class Accounts.AccountProviderRow : - AccountRow { - - private Manager accounts; - - public AccountProviderRow(Manager accounts, - Geary.AccountInformation account) { - base( - account, - // Translators: This label describes the program that - // created the account, e.g. an SSO service like GOA, or - // locally by Geary. - _("Account source"), - new Gtk.Label("") - ); - - this.accounts = accounts; - update(); - } - - public override void update() { - string? source = null; - bool enabled = false; - if (this.account.mediator is GoaMediator) { - source = _("GNOME Online Accounts"); - enabled = true; - } else { - source = _("Geary"); - } - - this.value.set_text(source); - this.set_activatable(enabled); - Gtk.StyleContext style = this.value.get_style_context(); - if (enabled) { - style.remove_class(Gtk.STYLE_CLASS_DIM_LABEL); - } else { - style.add_class(Gtk.STYLE_CLASS_DIM_LABEL); - } - } - - public override void activated(EditorServersPane pane) { - if (this.accounts.is_goa_account(this.account)) { - this.accounts.show_goa_account.begin( - account, pane.op_cancellable, - (obj, res) => { - try { - this.accounts.show_goa_account.end(res); - } catch (GLib.Error err) { - // XXX display an error to the user - debug( - "Failed to show GOA account \"%s\": %s", - account.id, - err.message - ); - } - }); - } - } - -} - - -private class Accounts.SaveDraftsRow : - AccountRow { +#if 0 +private class zccounts.SaveDraftsRow : Adw.SwitchRow { + public Geary.AccountInformation account { get; construct set; } public bool value_changed { get { return this.initial_value != this.value.state; } } - public bool initial_value { get; private set; } + public bool initial_value { get; construct set; } private Application.CommandStack commands; private GLib.Cancellable? cancellable; @@ -599,25 +403,22 @@ private class Accounts.SaveDraftsRow : public SaveDraftsRow(Geary.AccountInformation account, Application.CommandStack commands, GLib.Cancellable? cancellable) { - Gtk.Switch value = new Gtk.Switch(); - base( - account, - // Translators: This label describes an account - // preference. - _("Save draft email on server"), - value + Object( + account: account, + initial_value: account.save_drafts ); - update(); + this.commands = commands; this.cancellable = cancellable; - this.activatable = false; - this.initial_value = this.account.save_drafts; - this.account.notify["save-drafts"].connect(on_account_changed); - this.value.notify["active"].connect(on_activate); + this.account.notify["save-drafts"].connect(update); + this.notify["active"].connect(on_activate); + update(); } - public override void update() { - this.value.state = this.account.save_drafts; + private void update() { + //XXX GTK4 I think we need to guard this with an if to not activate the + // switch again + this.active = this.account.save_drafts; } private void on_activate() { @@ -630,11 +431,6 @@ private class Accounts.SaveDraftsRow : ); } } - - private void on_account_changed() { - update(); - } - } @@ -697,10 +493,6 @@ private class Accounts.ServiceHostRow : ServiceRow, ValidatingRow { - public Components.Validator validator { - get; protected set; - } - public bool has_changed { get { return this.value.text.strip() != get_entry_text(); @@ -850,11 +642,6 @@ private class Accounts.ServiceSecurityRow : private class Accounts.ServiceLoginRow : ServiceRow, ValidatingRow { - - public Components.Validator validator { - get; protected set; - } - public bool has_changed { get { return this.value.text.strip() != get_entry_text(); @@ -936,10 +723,9 @@ private class Accounts.ServiceLoginRow : string? label = null; if (this.service.credentials != null) { string method = "%s"; - Gtk.StyleContext value_style = this.value.get_style_context(); switch (this.service.credentials.supported_method) { case Geary.Credentials.Method.PASSWORD: - value_style.remove_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.value.remove_css_class("dim-label"); break; case Geary.Credentials.Method.OAUTH2: @@ -951,7 +737,7 @@ private class Accounts.ServiceLoginRow : // the service's login name. method = _("%s using OAuth2"); - value_style.add_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.value.add_css_class("dim-label"); break; } @@ -975,10 +761,6 @@ private class Accounts.ServicePasswordRow : ServiceRow, ValidatingRow { - public Components.Validator validator { - get; protected set; - } - public bool has_changed { get { return this.value.text.strip() != get_entry_text(); @@ -1116,3 +898,4 @@ private class Accounts.ServiceOutgoingAuthRow : } } +#endif diff --git a/src/client/accounts/accounts-editor.vala b/src/client/accounts/accounts-editor.vala index e498a8b1..8ba472ce 100644 --- a/src/client/accounts/accounts-editor.vala +++ b/src/client/accounts/accounts-editor.vala @@ -15,7 +15,7 @@ * management, account management and other common code for the panes. */ [GtkTemplate (ui = "/org/gnome/Geary/accounts_editor.ui")] -public class Accounts.Editor : Gtk.Dialog { +public class Accounts.Editor : Adw.Dialog { private const ActionEntry[] EDIT_ACTIONS = { @@ -35,10 +35,7 @@ public class Accounts.Editor : Gtk.Dialog { /** Returns the editor's associated client application instance. */ - public new Application.Client application { - get { return (Application.Client) base.get_application(); } - set { base.set_application(value); } - } + public Application.Client application { get; private set; } internal Manager accounts { get; private set; } @@ -48,49 +45,35 @@ public class Accounts.Editor : Gtk.Dialog { private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); - [GtkChild] private unowned Gtk.Overlay notifications_pane; + [GtkChild] private unowned Adw.ToastOverlay toast_overlay; - [GtkChild] private unowned Gtk.Stack editor_panes; + [GtkChild] private unowned Adw.NavigationView view; private EditorListPane editor_list_pane; - private Gee.LinkedList editor_pane_stack = - new Gee.LinkedList(); - - public Editor(Application.Client application, Gtk.Window parent) { + public Editor(Application.Client application) { this.application = application; - this.transient_for = parent; - this.icon_name = Config.APP_ID; this.accounts = application.controller.account_manager; this.certificates = application.controller.certificate_manager; - // Can't set this in Glade 3.22.1 :( - this.get_content_area().border_width = 0; - this.accounts = application.controller.account_manager; this.edit_actions.add_action_entries(EDIT_ACTIONS, this); insert_action_group(Action.Edit.GROUP_NAME, this.edit_actions); this.editor_list_pane = new EditorListPane(this); - push(this.editor_list_pane); + push_pane(this.editor_list_pane); update_command_actions(); - - if (this.accounts.size > 1) { - this.default_height = 650; - this.default_width = 800; - } else { - // Welcome dialog - this.default_width = 600; - } } - public override bool key_press_event(Gdk.EventKey event) { + [GtkCallback] + private bool on_key_pressed(uint keyval, uint keycode, Gdk.ModifierType mod_state) { bool ret = Gdk.EVENT_PROPAGATE; + // XXX GTK4 - we'll need to disable the esc behavio in adwnavigationview and then do it manually here // Allow the user to use Esc, Back and Alt+arrow keys to // navigate between panes. If a pane is executing a long // running operation, only allow Esc and use it to cancel the @@ -98,51 +81,15 @@ public class Accounts.Editor : Gtk.Dialog { EditorPane? current_pane = get_current_pane(); if (current_pane != null && current_pane != this.editor_list_pane) { - Gdk.ModifierType state = ( - event.state & Gtk.accelerator_get_default_mod_mask() - ); - bool is_ltr = (get_direction() == Gtk.TextDirection.LTR); - switch (event.keyval) { - case Gdk.Key.Escape: + if (keyval == Gdk.Key.Escape) { if (current_pane.is_operation_running) { current_pane.cancel_operation(); } else { - pop(); + pop_pane(); } ret = Gdk.EVENT_STOP; - break; - - case Gdk.Key.Back: - if (!current_pane.is_operation_running) { - pop(); - ret = Gdk.EVENT_STOP; - } - break; - - case Gdk.Key.Left: - if (!current_pane.is_operation_running && - state == Gdk.ModifierType.MOD1_MASK && - is_ltr) { - pop(); - ret = Gdk.EVENT_STOP; - } - break; - - case Gdk.Key.Right: - if (!current_pane.is_operation_running && - state == Gdk.ModifierType.MOD1_MASK && - !is_ltr) { - pop(); - ret = Gdk.EVENT_STOP; - } - break; } - - } - - if (ret != Gdk.EVENT_STOP) { - ret = base.key_press_event(event); } return ret; @@ -151,41 +98,20 @@ public class Accounts.Editor : Gtk.Dialog { /** * Adds and shows a new pane in the editor. */ - internal void push(EditorPane pane) { - // Since we keep old, already-popped panes around (see pop for - // details), when a new pane is pushed on they need to be - // truncated. - EditorPane current = get_current_pane(); - int target_length = this.editor_pane_stack.index_of(current) + 1; - while (target_length < this.editor_pane_stack.size) { - EditorPane old = this.editor_pane_stack.remove_at(target_length); - this.editor_panes.remove(old); - } - - // Now push the new pane on - this.editor_pane_stack.add(pane); - this.editor_panes.add(pane); - this.editor_panes.set_visible_child(pane); + internal void push_pane(EditorPane pane) { + this.view.push(pane); } /** * Removes the current pane from the editor, showing the last one. */ - internal void pop() { - // One can't simply remove old panes for the GTK stack since - // there won't be any transition between them - the old one - // will simply disappear. So we need to keep old, popped panes - // around until a new one is pushed on. - EditorPane current = get_current_pane(); - int prev_index = this.editor_pane_stack.index_of(current) - 1; - EditorPane prev = this.editor_pane_stack.get(prev_index); - this.editor_panes.set_visible_child(prev); + internal bool pop_pane() { + return this.view.pop(); } /** Displays an in-app notification in the dialog. */ - internal void add_notification(Components.InAppNotification notification) { - this.notifications_pane.add_overlay(notification); - notification.show(); + internal void add_toast(Adw.Toast toast) { + this.toast_overlay.add_toast(toast); } /** @@ -202,14 +128,14 @@ public class Accounts.Editor : Gtk.Dialog { throws Application.CertificateManagerError { try { yield this.certificates.prompt_pin_certificate( - this, account, service, endpoint, true, cancellable + get_root() as Gtk.Window, account, service, endpoint, true, cancellable ); } catch (Application.CertificateManagerError.UNTRUSTED err) { throw err; } catch (Application.CertificateManagerError.STORE_FAILED err) { // XXX show error info bar rather than a notification? - add_notification( - new Components.InAppNotification( + add_toast( + new Adw.Toast( // Translators: In-app notification label, when // the app had a problem pinning an otherwise // untrusted TLS certificate @@ -225,7 +151,7 @@ public class Accounts.Editor : Gtk.Dialog { /** Removes an account from the editor. */ internal void remove_account(Geary.AccountInformation account) { - this.editor_panes.set_visible_child(this.editor_list_pane); + this.view.pop_to_page(this.editor_list_pane); this.editor_list_pane.remove_account(account); } @@ -244,7 +170,7 @@ public class Accounts.Editor : Gtk.Dialog { } private inline EditorPane? get_current_pane() { - return this.editor_panes.get_visible_child() as EditorPane; + return this.view.visible_page as EditorPane; } private inline GLib.SimpleAction get_action(string name) { @@ -264,51 +190,18 @@ public class Accounts.Editor : Gtk.Dialog { pane.redo(); } } - - [GtkCallback] - private void on_pane_changed() { - EditorPane? visible = get_current_pane(); - Gtk.Widget? header = null; - if (visible != null) { - // Do this in an idle callback since it's not 100% - // reliable to just call it here for some reason. :( - GLib.Idle.add(() => { - visible.initial_widget.grab_focus(); - return GLib.Source.REMOVE; - }); - header = visible.get_header(); - } - set_titlebar(header); - update_command_actions(); - } - } -// XXX I'd really like to make EditorPane an abstract class, -// AccountPane an abstract class extending that, and the four concrete -// panes extend those, but the GTK+ Builder XML template system -// requires a template class to designate its immediate parent -// class. I.e. if accounts-editor-list-pane.ui specifies GtkGrid as -// the parent of EditorListPane, then it much exactly be that and not -// an instance of EditorPane, even if that extends GtkGrid. As a -// result, both EditorPane and AccountPane must both be interfaces so -// that the concrete pane classes can derive from GtkGrid directly, -// and everything becomes horrible. See GTK+ Issue #1151: -// https://gitlab.gnome.org/GNOME/gtk/issues/1151 - /** * Base interface for panes that can be shown by the accounts editor. */ -internal interface Accounts.EditorPane : Gtk.Grid { +internal abstract class Accounts.EditorPane : Adw.NavigationPage { /** The editor displaying this pane. */ internal abstract weak Accounts.Editor editor { get; set; } - /** The editor displaying this pane. */ - internal abstract Gtk.Widget initial_widget { get; } - /** * Determines if a long running operation is being executed. * @@ -327,9 +220,6 @@ internal interface Accounts.EditorPane : Gtk.Grid { */ internal abstract GLib.Cancellable? op_cancellable { get; protected set; } - /** The GTK header bar to display for this pane. */ - internal abstract Gtk.HeaderBar get_header(); - /** * Cancels this pane's current operation, any. * @@ -376,21 +266,13 @@ internal interface Accounts.AccountPane : EditorPane { this.account.changed.disconnect(on_account_changed); } - /** - * Called when an account has changed. - * - * By default, updates the editor's header subtitle. - */ - private void account_changed() { + private void on_account_changed() { update_header(); } private inline void update_header() { - get_header().subtitle = this.account.display_name; - } - - private void on_account_changed() { - account_changed(); + // XXX GTK4 - this was subtitle before, will need to make the title subtitle + this.title = this.account.display_name; } } diff --git a/src/client/accounts/accounts-mailbox-editor-dialog.vala b/src/client/accounts/accounts-mailbox-editor-dialog.vala new file mode 100644 index 00000000..54f97c45 --- /dev/null +++ b/src/client/accounts/accounts-mailbox-editor-dialog.vala @@ -0,0 +1,128 @@ +/* + * Copyright 2018-2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A simple dialog that allows adding/editing Mailboxes (e.g. when configuring + * sender addresses). + */ +[GtkTemplate (ui = "/org/gnome/Geary/accounts-mailbox-editor-dialog.ui")] +internal class Accounts.MailboxEditorDialog : Adw.Dialog { + + + [GtkChild] private unowned Adw.EntryRow name_row; + [GtkChild] private unowned Adw.EntryRow address_row; + [GtkChild] private unowned Gtk.Button apply_button; + [GtkChild] private unowned Gtk.Button remove_button; + private Components.EmailValidator address_validator; + + private bool changed = false; + + + /** The display name for the address */ + public string display_name { get; construct set; default = ""; } + + /** The raw email address */ + public string address { get; construct set; default = ""; } + + + /** Fired if the user pressed "Add"/"Apply" with the new details */ + public signal void apply(Geary.RFC822.MailboxAddress mailbox); + + /** Fired if the user requested to remove the address */ + public signal void remove(); + + + static construct { + install_action("apply", null, (Gtk.WidgetActionActivateFunc) action_apply); + install_action("remove", null, (Gtk.WidgetActionActivateFunc) action_remove); + } + + + construct { + this.name_row.text = this.display_name; + this.address_row.text = this.address; + this.changed = false; + + this.address_validator = + new Components.EmailValidator(this.address_row); + this.address_validator.changed.connect((validator) => { + action_set_enabled("add", this.changed && input_is_valid()); + }); + } + + + /** + * Creates a MailboxEditorDialog for creating a new mailbox. + * @param display_name A suggestion for the name + */ + public MailboxEditorDialog.for_new(string? display_name) { + Object( + display_name: display_name ?? "", + address: "" + ); + + // Cange "Apply" to "Add" in this case, since that matches better + this.apply_button.label = _("_Add"); + + // Can't remove an address that doesn't exist yet + action_set_enabled("remove", false); + } + + public MailboxEditorDialog.for_existing(Geary.RFC822.MailboxAddress mailbox, + bool can_remove) { + Object( + display_name: mailbox.name ?? "", + address: mailbox.address + ); + + action_set_enabled("remove", can_remove); + } + + [GtkCallback] + private void on_name_changed(Gtk.Editable editable) { + var new_name = this.name_row.text.strip(); + if (new_name != this.display_name) { + this.display_name = new_name; + this.changed = true; + } + } + + [GtkCallback] + private void on_address_changed(Gtk.Editable editable) { + this.address = this.address_row.text.strip(); + var new_address = this.address_row.text.strip(); + if (new_address != this.address) { + this.address = new_address; + this.changed = true; + } + } + + [GtkCallback] + private void on_entry_activate() { + activate_action("add", null); + } + + private bool input_is_valid() { + return this.address_validator.state == Components.Validator.Validity.INDETERMINATE + || this.address_validator.is_valid; + } + + private void action_apply(string action_name, Variant? param) { + if (!input_is_valid()) { + debug("Tried to add mailbox, but email was invalid"); + return; + } + + apply( + new Geary.RFC822.MailboxAddress(this.display_name, this.address) + ); + } + + private void action_remove(string action_name, Variant? param) { + remove(); + } +} diff --git a/src/client/accounts/accounts-service-information-widget.vala b/src/client/accounts/accounts-service-information-widget.vala new file mode 100644 index 00000000..cf11a45a --- /dev/null +++ b/src/client/accounts/accounts-service-information-widget.vala @@ -0,0 +1,173 @@ +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2018-2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A widget for editing a {@link Geary.ServiceInformation} object. + */ +[GtkTemplate (ui = "/org/gnome/Geary/accounts-service-information-widget.ui")] +internal class Accounts.ServiceInformationWidget : Adw.PreferencesGroup { + + public Geary.ServiceInformation service { + get { return this._service; } + set { + this._service = value; + this.service_mutable = new Geary.ServiceInformation.copy(value); + update_details(); + value.notify.connect((obj, pspec) => { update_details(); }); + } + } + private Geary.ServiceInformation _service; + + // A copy of the original that can be without breaking the original + public Geary.ServiceInformation service_mutable { get ; private set; } + + public Components.ValidatorGroup validators { get; construct set; } + + [GtkChild] private unowned Adw.EntryRow host_row; + [GtkChild] private unowned TlsComboRow security_row; + [GtkChild] private unowned Adw.ComboRow credentials_requirement_row; + [GtkChild] private unowned Adw.EntryRow login_name_row; + [GtkChild] private unowned Adw.PasswordEntryRow password_row; + + + static construct { + typeof(TlsComboRow).ensure(); + typeof(Components.ValidatorGroup).ensure(); + typeof(Components.Validator).ensure(); + } + + + /** + * Sets whether editing the information is possible + */ + public void set_editable(bool editable) { + this.sensitive = editable; + update_details(); + } + + private void update_details() { + update_host_row(this.host_row, this.service_mutable); + this.security_row.method = this.service_mutable.transport_security; + update_auth(this.service_mutable); + } + + private void update_host_row(Adw.EntryRow row, Geary.ServiceInformation service) { + row.title = host_label_for_protocol(service.protocol); + + row.text = service.host ?? ""; + if (!Geary.String.is_empty(service.host)) { + // Only show the port if it not the appropriate default port + uint16 port = service.port; + if (port != service.get_default_port()) { + row.text = "%s:%d".printf(service.host, service.port); + } + } + } + + private string host_label_for_protocol(Geary.Protocol protocol) { + switch (protocol) { + case Geary.Protocol.IMAP: + // Translators: This label describes the host name or IP + // address and port used by an account's IMAP service. + return _("IMAP Server"); + + case Geary.Protocol.SMTP: + // Translators: This label describes the host name or IP + // address and port used by an account's SMTP service. + return _("SMTP Server"); + } + + return _("Unknown Protocol"); + } + + private void update_login_name_row(Adw.EntryRow row, + Geary.ServiceInformation service) { + // Translators: Label used when no auth scheme is used + // by an account's IMAP or SMTP service. + row.text = _("None"); + + // If we have credentials, we can do better + if (service.credentials != null) { + switch (service.credentials.supported_method) { + case Geary.Credentials.Method.PASSWORD: + row.text = service.credentials.user; + break; + + case Geary.Credentials.Method.OAUTH2: + // Add a suffix for OAuth2 auth so people know they + // shouldn't expect to be prompted for a password + + // Translators: Label used when an account's IMAP or + // SMTP service uses OAuth2. The string replacement is + // the service's login name. + row.text = _("%s using OAuth2").printf(service.credentials.user ?? ""); + break; + } + } + + // If we rely on the credentials of the incoming server, notify the user of that + if (service.protocol == Geary.Protocol.SMTP && + service.credentials_requirement == + Geary.Credentials.Requirement.USE_INCOMING) { + row.text = _("Use receiving server login"); + } + } + + private void update_password_row(Adw.PasswordEntryRow row, + Geary.ServiceInformation service) { + if (service.credentials != null) { + row.text = service.credentials.token ?? ""; + } else { + row.text = ""; + } + + // If we're not enabled, the "Show Password" button is insensitive too + // so just hide the row + if (!this.sensitive) + row.visible = false; + } + + [GtkCallback] + private void on_host_row_changed(Gtk.Editable editable) { + } + + private void update_auth(Geary.ServiceInformation service) { + bool is_smtp = (service.protocol == Geary.Protocol.SMTP); + this.credentials_requirement_row.visible = is_smtp; + + if (is_smtp) { + this.credentials_requirement_row.selected = service.credentials_requirement; + + bool needs_login = + (service.credentials_requirement == Geary.Credentials.Requirement.CUSTOM); + this.login_name_row.visible = needs_login; + this.password_row.visible = needs_login; + } + + update_login_name_row(this.login_name_row, this.service_mutable); + update_password_row(this.password_row, this.service_mutable); + } + + [GtkCallback] + private static string outgoing_auth_to_string(Adw.EnumListItem item, + Geary.Credentials.Requirement requirement) { + return requirement.to_string(); + } + + [GtkCallback] + private void on_validators_changed(Components.ValidatorGroup validators, + Components.Validator validator) { + //XXX what do we do here? + } + + [GtkCallback] + private void on_validators_activated(Components.ValidatorGroup validators, + Components.Validator validator) { + //XXX what do we do here? + } +} diff --git a/src/client/accounts/accounts-signature-web-view.vala b/src/client/accounts/accounts-signature-web-view.vala index d424dd64..6a901a5e 100644 --- a/src/client/accounts/accounts-signature-web-view.vala +++ b/src/client/accounts/accounts-signature-web-view.vala @@ -22,8 +22,9 @@ public class Accounts.SignatureWebView : Components.WebView { public SignatureWebView(Application.Configuration config) { - base(config); + base(config, null); this.user_content_manager.add_script(SignatureWebView.app_script); + add_css_class("geary-signature"); } } diff --git a/src/client/accounts/accounts-tls-combo-row.vala b/src/client/accounts/accounts-tls-combo-row.vala new file mode 100644 index 00000000..a0c2625a --- /dev/null +++ b/src/client/accounts/accounts-tls-combo-row.vala @@ -0,0 +1,55 @@ +/* + * Copyright 2025 Niels De Graef + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +[GtkTemplate (ui = "/org/gnome/Geary/accounts-tls-combo-row.ui")] +internal class Accounts.TlsComboRow : Adw.ComboRow { + + private const string INSECURE_ICON = "channel-insecure-symbolic"; + private const string SECURE_ICON = "channel-secure-symbolic"; + + + public Geary.TlsNegotiationMethod method { + get { return ((Adw.EnumListItem) this.selected_item).value; } + set { this.selected = value; } + } + + [GtkCallback] + private void on_factory_setup(Gtk.SignalListItemFactory factory, + GLib.Object object) { + unowned var item = (Gtk.ListItem) object; + + var image = new Gtk.Image(); + + var label = new Gtk.Label(null); + label.xalign = 1.0f; + + var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + box.append(image); + box.append(label); + + item.child = box; + } + + [GtkCallback] + private void on_factory_bind(Gtk.SignalListItemFactory factory, + GLib.Object object) { + unowned var item = (Gtk.ListItem) object; + unowned var enum_item = (Adw.EnumListItem) item.item; + var method = (Geary.TlsNegotiationMethod) enum_item.get_value(); + + unowned var box = (Gtk.Box) item.child; + + unowned var image = (Gtk.Image) box.get_first_child(); + if (method == Geary.TlsNegotiationMethod.NONE) + image.icon_name = "channel-insecure-symbolic"; + else + image.icon_name = "channel-secure-symbolic"; + + unowned var label = (Gtk.Label) image.get_next_sibling(); + label.label = method.to_string(); + } +} diff --git a/src/client/application/application-attachment-manager.vala b/src/client/application/application-attachment-manager.vala index aa8cb1b4..bae6d19f 100644 --- a/src/client/application/application-attachment-manager.vala +++ b/src/client/application/application-attachment-manager.vala @@ -84,67 +84,70 @@ public class Application.AttachmentManager : GLib.Object { * else false. */ public async bool save_buffer(string display_name, - Geary.Memory.Buffer buffer, - GLib.Cancellable? cancellable) { - Gtk.FileChooserNative dialog = new_save_chooser(SAVE); - dialog.set_current_name(display_name); + Geary.Memory.Buffer buffer, + GLib.Cancellable? cancellable) { + var dialog = new Gtk.FileDialog(); + dialog.initial_name = display_name; + dialog.initial_folder = download_dir(); - string? destination_uri = null; - if (dialog.run() == Gtk.ResponseType.ACCEPT) { - destination_uri = dialog.get_uri(); + File? destination = null; + try { + destination = yield dialog.save(this.parent, cancellable); + } catch (Error err) { + //XXX GTK4 check if cancelled is accidentally caught here as well + warning("Couldn't select file to save attachment: %s", err.message); + return false; } - dialog.destroy(); - bool succeeded = false; - if (!Geary.String.is_empty_or_whitespace(destination_uri)) { - succeeded = yield check_and_write( - buffer, GLib.File.new_for_uri(destination_uri), cancellable - ); - } - return succeeded; + return yield check_and_write(buffer, destination, cancellable); } private async bool save_all(Gee.Collection attachments, GLib.Cancellable? cancellable) { - var dialog = new_save_chooser(SELECT_FOLDER); - string? destination_uri = null; - if (dialog.run() == Gtk.ResponseType.ACCEPT) { - destination_uri = dialog.get_uri(); + var dialog = new Gtk.FileDialog(); + dialog.initial_file = download_dir(); + + File? destination_dir = null; + try { + destination_dir = yield dialog.select_folder(this.parent, cancellable); + } catch (Error err) { + //XXX GTK4 check if cancelled is accidentally caught here as well + warning("Couldn't select folder for saving attachments: %s", err.message); + return false; } - dialog.destroy(); + + if (destination_dir == null) + return false; bool succeeded = false; - if (!Geary.String.is_empty_or_whitespace(destination_uri)) { - var destination_dir = GLib.File.new_for_uri(destination_uri); - foreach (Geary.Attachment attachment in attachments) { - GLib.File? destination = null; - try { - destination = destination_dir.get_child_for_display_name( - yield attachment.get_safe_file_name( - AttachmentManager.untitled_file_name - ) - ); - } catch (GLib.IOError.CANCELLED err) { - // Everything is going to fail from now on, so get - // out of here - succeeded = false; - break; - } catch (GLib.Error err) { - warning( - "Error determining file system name for \"%s\": %s", - attachment.file.get_uri(), err.message - ); - handle_error(err); - } - var content = yield open_buffer(attachment, cancellable); - if (content != null && - destination != null) { - succeeded &= yield check_and_write( - content, destination, cancellable - ); - } else { - succeeded = false; - } + foreach (Geary.Attachment attachment in attachments) { + GLib.File? destination = null; + try { + destination = destination_dir.get_child_for_display_name( + yield attachment.get_safe_file_name( + AttachmentManager.untitled_file_name + ) + ); + } catch (GLib.IOError.CANCELLED err) { + // Everything is going to fail from now on, so get + // out of here + succeeded = false; + break; + } catch (GLib.Error err) { + warning( + "Error determining file system name for \"%s\": %s", + attachment.file.get_uri(), err.message + ); + handle_error(err); + } + var content = yield open_buffer(attachment, cancellable); + if (content != null && + destination != null) { + succeeded &= yield check_and_write( + content, destination, cancellable + ); + } else { + succeeded = false; } } return succeeded; @@ -229,14 +232,18 @@ public class Application.AttachmentManager : GLib.Object { "The file already exists in “%s”. Replacing it will overwrite its contents." ).printf(parent_name); - ConfirmationDialog dialog = new ConfirmationDialog( - this.parent, - primary, - secondary, - _("_Replace"), - "destructive-action" + var dialog = new Adw.AlertDialog(primary, secondary); + dialog.add_responses( + "replace", _("_Replace"), + "cancel", _("_Cancel"), + null ); - return (dialog.run() == Gtk.ResponseType.OK); + dialog.default_response = "cancel"; + dialog.close_response = "cancel"; + dialog.set_response_appearance("replace", Adw.ResponseAppearance.DESTRUCTIVE); + string response = yield dialog.choose(this.parent, cancellable); + + return (response == "replace"); } private async void write_buffer_to_file(Geary.Memory.Buffer buffer, @@ -263,20 +270,11 @@ public class Application.AttachmentManager : GLib.Object { } } - private inline Gtk.FileChooserNative new_save_chooser(Gtk.FileChooserAction action) { - Gtk.FileChooserNative dialog = new Gtk.FileChooserNative( - null, - this.parent, - action, - Stock._SAVE, - Stock._CANCEL - ); + private File? download_dir() { var download_dir = GLib.Environment.get_user_special_dir(DOWNLOAD); - if (!Geary.String.is_empty_or_whitespace(download_dir)) { - dialog.set_current_folder(download_dir); - } - dialog.set_local_only(false); - return dialog; + if (Geary.String.is_empty_or_whitespace(download_dir)) + return null; + return File.new_for_path(download_dir); } private inline void handle_error(GLib.Error error) { diff --git a/src/client/application/application-certificate-manager.vala b/src/client/application/application-certificate-manager.vala index bb1511a3..dac5c43e 100644 --- a/src/client/application/application-certificate-manager.vala +++ b/src/client/application/application-certificate-manager.vala @@ -118,11 +118,11 @@ public class Application.CertificateManager : GLib.Object { GLib.Cancellable? cancellable) throws CertificateManagerError { CertificateWarningDialog dialog = new CertificateWarningDialog( - parent, account, service, endpoint, is_validation + account, service, endpoint, is_validation ); bool save = false; - switch (dialog.run()) { + switch (yield dialog.run(parent)) { case CertificateWarningDialog.Result.TRUST: // noop break; diff --git a/src/client/application/application-client.vala b/src/client/application/application-client.vala index 6c8d6548..90218e00 100644 --- a/src/client/application/application-client.vala +++ b/src/client/application/application-client.vala @@ -10,7 +10,7 @@ /** * The client application's main point of entry and desktop integration. */ -public class Application.Client : Gtk.Application { +public class Application.Client : Adw.Application { public const string NAME = "Geary" + Config.NAME_SUFFIX; public const string RESOURCE_BASE_PATH = "/org/gnome/Geary"; @@ -222,7 +222,6 @@ public class Application.Client : Gtk.Application { private File exec_dir; private string binary; - private Gtk.CssProvider single_key_shortcuts = new Gtk.CssProvider(); private GLib.Cancellable controller_cancellable = new GLib.Cancellable(); private Components.Inspector? inspector = null; private Geary.Nonblocking.Mutex controller_mutex = new Geary.Nonblocking.Mutex(); @@ -348,9 +347,6 @@ public class Application.Client : Gtk.Application { // Calls Gtk.init(), amongst other things base.startup(); - Hdy.init(); - Hdy.StyleManager.get_default().set_color_scheme( - Hdy.ColorScheme.PREFER_LIGHT); this.engine = new Geary.Engine(get_resource_directory()); this.config = new Configuration(SCHEMA_ID); @@ -378,27 +374,21 @@ public class Application.Client : Gtk.Application { add_edit_accelerators(Action.Edit.REDO, { "Z" }); add_edit_accelerators(Action.Edit.UNDO, { "Z" }); - // Load Geary GTK CSS - var provider = new Gtk.CssProvider(); - Gtk.StyleContext.add_provider_for_screen( - Gdk.Display.get_default().get_default_screen(), - provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); - load_css(provider, - "resource:///org/gnome/Geary/geary.css"); + //XXX GTK4 key shortcut themes aren't supported yet: https://gitlab.gnome.org/GNOME/gtk/-/issues/1669#note_1735942 +#if 0 + // Load Geary CSS for single key shortcuts load_css(this.single_key_shortcuts, "resource:///org/gnome/Geary/single-key-shortcuts.css"); update_single_key_shortcuts(); this.config.notify[Configuration.SINGLE_KEY_SHORTCUTS].connect( on_single_key_shortcuts_toggled ); +#endif MainWindow.add_accelerators(this); Composer.Editor.add_accelerators(this); Composer.Widget.add_accelerators(this); Components.Inspector.add_accelerators(this); - Components.PreferencesWindow.add_accelerators(this); Dialogs.ProblemDetailsDialog.add_accelerators(this); // Manually place a hold on the application otherwise the @@ -432,7 +422,7 @@ public class Application.Client : Gtk.Application { // thing down if it takes too long to complete int64 start_usec = get_monotonic_time(); while (!controller_closed) { - Gtk.main_iteration(); + MainContext.default().iteration(false); int64 delta_usec = get_monotonic_time() - start_usec; if (delta_usec >= FORCE_SHUTDOWN_USEC) { @@ -553,12 +543,11 @@ public class Application.Client : Gtk.Application { public async void show_accounts() { yield this.present(); - Accounts.Editor editor = new Accounts.Editor( - this, get_active_main_window() - ); - editor.run(); - editor.destroy(); - this.controller.expunge_accounts.begin(); + Accounts.Editor editor = new Accounts.Editor(this); + editor.present(get_active_main_window()); + editor.closed.connect((editor) => { + this.controller.expunge_accounts.begin(); + }); } /** @@ -667,9 +656,10 @@ public class Application.Client : Gtk.Application { if (this.inspector == null) { this.inspector = new Components.Inspector(this); - this.inspector.destroy.connect(() => { - this.inspector = null; - }); + this.inspector.close_request.connect(() => { + this.inspector = null; + return true; + }); // Create a new window group for the inspector so it is // not affected by the app's modal dialogs @@ -685,11 +675,11 @@ public class Application.Client : Gtk.Application { public async void show_preferences() { yield this.present(); - Components.PreferencesWindow prefs = new Components.PreferencesWindow( - get_active_main_window(), + var prefs = new Components.PreferencesDialog( + this, this.controller.plugins ); - prefs.show(); + prefs.present(get_active_main_window()); } public async void new_composer(Geary.RFC822.MailboxAddress? to = null) { @@ -820,10 +810,9 @@ public class Application.Client : Gtk.Application { uri_ = "http://" + uri; } + var launcher = new Gtk.UriLauncher(uri_); try { - Gtk.show_uri_on_window( - get_active_window(), uri_, Gdk.CURRENT_TIME - ); + yield launcher.launch(get_active_window(), null); } catch (GLib.Error err) { this.controller.report_problem(new Geary.ProblemReport(err)); } @@ -837,8 +826,10 @@ public class Application.Client : Gtk.Application { * prompted about and if cancelled, will cancel shut-down here. */ public new void quit() { - if (this.controller == null || - this.controller.check_open_composers()) { + //XXX GTK4 this is now async, need to figure out how to do this + // if (this.controller == null || + // this.controller.check_open_composers()) { + if (this.controller == null) { this.last_active_main_window = null; base.quit(); } @@ -908,7 +899,9 @@ public class Application.Client : Gtk.Application { private MainWindow new_main_window(bool select_first_inbox) { MainWindow window = new MainWindow(this); this.controller.register_window(window); - window.focus_in_event.connect(on_main_window_focus_in); + Gtk.EventControllerFocus focus_controller = new Gtk.EventControllerFocus(); + focus_controller.enter.connect(on_main_window_focus_enter); + ((Gtk.Widget) window).add_controller(focus_controller); if (select_first_inbox) { if (!window.select_first_inbox(true)) { // The first inbox wasn't selected, so the account is @@ -958,11 +951,10 @@ public class Application.Client : Gtk.Application { open_failed = true; warning("Error creating controller: %s", err.message); var dialog = new Dialogs.ProblemDetailsDialog( - null, this, new Geary.ProblemReport(err) ); - dialog.show(); + dialog.present(null); } if (mutex_token != Geary.Nonblocking.Mutex.INVALID_TOKEN) { @@ -1100,20 +1092,23 @@ public class Application.Client : Gtk.Application { set_accels_for_action("app." + action, accelerators); } + //XXX GTK4 key shortcut themes aren't supported yet: https://gitlab.gnome.org/GNOME/gtk/-/issues/1669#note_1735942 +#if 0 private void update_single_key_shortcuts() { if (this.config.single_key_shortcuts) { - Gtk.StyleContext.add_provider_for_screen( - Gdk.Display.get_default().get_default_screen(), + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), this.single_key_shortcuts, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); } else { - Gtk.StyleContext.remove_provider_for_screen( - Gdk.Display.get_default().get_default_screen(), + Gtk.StyleContext.remove_provider_for_display( + Gdk.Display.get_default(), this.single_key_shortcuts ); } } +#endif private void load_css(Gtk.CssProvider provider, string resource_uri) { provider.parsing_error.connect(on_css_parse_error); @@ -1197,7 +1192,8 @@ public class Application.Client : Gtk.Application { private void on_activate_help() { try { if (this.is_installed) { - this.show_uri.begin("help:geary"); + var launcher = new Gtk.UriLauncher("help:geary"); + launcher.launch.begin(get_active_window(), null); } else { Pid pid; File exec_dir = this.exec_dir; @@ -1217,17 +1213,10 @@ public class Application.Client : Gtk.Application { } } catch (Error error) { debug("Error showing help: %s", error.message); - Gtk.Dialog dialog = new Gtk.Dialog.with_buttons( - "Error", - get_active_window(), - Gtk.DialogFlags.DESTROY_WITH_PARENT, - Stock._CLOSE, Gtk.ResponseType.CLOSE, null); - dialog.response.connect(() => { dialog.destroy(); }); - dialog.get_content_area().add( - new Gtk.Label("Error showing help: %s".printf(error.message)) + Adw.AlertDialog dialog = new Adw.AlertDialog("Error", + "Error showing help: %s".printf(error.message) ); - dialog.show_all(); - dialog.run(); + dialog.present(get_active_window()); } } @@ -1243,13 +1232,11 @@ public class Application.Client : Gtk.Application { } } - private bool on_main_window_focus_in(Gtk.Widget widget, - Gdk.EventFocus event) { - MainWindow? main = widget as MainWindow; + private void on_main_window_focus_enter(Gtk.EventControllerFocus focus_controller) { + MainWindow? main = focus_controller.get_widget() as MainWindow; if (main != null) { this.last_active_main_window = main; } - return Gdk.EVENT_PROPAGATE; } private void on_window_removed(Gtk.Window window) { @@ -1271,22 +1258,25 @@ public class Application.Client : Gtk.Application { } } + //XXX GTK4 key shortcut themes aren't supported yet: https://gitlab.gnome.org/GNOME/gtk/-/issues/1669#note_1735942 +#if 0 private void on_single_key_shortcuts_toggled() { update_single_key_shortcuts(); } +#endif - private void on_css_parse_error(Gtk.CssSection section, GLib.Error error) { - uint start = section.get_start_line(); - uint end = section.get_end_line(); - if (start == end) { + private void on_css_parse_error(Gtk.CssProvider provider, Gtk.CssSection section, GLib.Error error) { + var start = section.get_start_location(); + var end = section.get_end_location(); + if (start.lines == end.lines) { warning( - "Error parsing %s:%u: %s", - section.get_file().get_uri(), start, error.message + "Error parsing %s:%"+size_t.FORMAT+": %s", + section.get_file().get_uri(), start.lines, error.message ); } else { warning( - "Error parsing %s:%u-%u: %s", - section.get_file().get_uri(), start, end, error.message + "Error parsing %s:%"+size_t.FORMAT+"-%"+size_t.FORMAT+": %s", + section.get_file().get_uri(), start.lines, end.lines, error.message ); } } diff --git a/src/client/application/application-contact-store.vala b/src/client/application/application-contact-store.vala index 67e5dd33..06ad65d7 100644 --- a/src/client/application/application-contact-store.vala +++ b/src/client/application/application-contact-store.vala @@ -126,8 +126,8 @@ public class Application.ContactStore : Geary.BaseObject { Contact result = yield get_contact( individual, null, cancellable ); - foreach (Geary.RFC822.MailboxAddress mailbox - in result.email_addresses) { + for (uint i = 0; i < result.email_addresses.get_n_items(); i++) { + var mailbox = (Geary.RFC822.MailboxAddress) result.email_addresses.get_item(i); seen.add(to_cache_key(mailbox.address)); } results.add(result); @@ -140,8 +140,8 @@ public class Application.ContactStore : Geary.BaseObject { Contact result = yield get_contact( individual, null, cancellable ); - foreach (Geary.RFC822.MailboxAddress mailbox - in result.email_addresses) { + for (uint i = 0; i < result.email_addresses.get_n_items(); i++) { + var mailbox = (Geary.RFC822.MailboxAddress) result.email_addresses.get_item(i); seen.add(to_cache_key(mailbox.address)); } results.add(result); @@ -166,8 +166,8 @@ public class Application.ContactStore : Geary.BaseObject { Contact result = yield load( contact.get_rfc822_address(), cancellable ); - foreach (Geary.RFC822.MailboxAddress mailbox - in result.email_addresses) { + for (uint i = 0; i < result.email_addresses.get_n_items(); i++) { + var mailbox = (Geary.RFC822.MailboxAddress) result.email_addresses.get_item(i); seen.add(to_cache_key(mailbox.address)); } results.add(result); diff --git a/src/client/application/application-contact.vala b/src/client/application/application-contact.vala index 71397cf0..3ee1b43a 100644 --- a/src/client/application/application-contact.vala +++ b/src/client/application/application-contact.vala @@ -55,24 +55,23 @@ public class Application.Contact : Geary.BaseObject { public bool load_remote_resources { get; private set; } /** The set of email addresses associated with this contact. */ - public Gee.Collection email_addresses { + public GLib.ListModel email_addresses { get { - Gee.Collection? addrs = - this._email_addresses; - if (addrs == null) { - addrs = new Gee.LinkedList(); + if (this._email_addresses == null) { + var addrs = new GLib.ListStore(typeof(Geary.RFC822.MailboxAddress)); foreach (Folks.EmailFieldDetails email in this.individual.email_addresses) { - addrs.add(new Geary.RFC822.MailboxAddress( - this.display_name, email.value - )); + var mailbox_addr = new Geary.RFC822.MailboxAddress( + this.display_name, email.value + ); + addrs.append(mailbox_addr); } this._email_addresses = addrs; } return this._email_addresses; } } - private Gee.Collection? _email_addresses = null; + private GLib.ListModel? _email_addresses = null; /** Fired when the contact has changed in some way. */ @@ -142,14 +141,15 @@ public class Application.Contact : Geary.BaseObject { } if (this.display_name != other.display_name || - this.email_addresses.size != other.email_addresses.size) { + this.email_addresses.get_n_items() != other.email_addresses.get_n_items()) { return false; } - foreach (Geary.RFC822.MailboxAddress this_addr in this.email_addresses) { + for (uint i = 0; i < this.email_addresses.get_n_items(); i++) { + var this_addr = (Geary.RFC822.MailboxAddress) this.email_addresses.get_item(i); bool found = false; - foreach (Geary.RFC822.MailboxAddress other_addr - in other.email_addresses) { + for (uint j = 0; j < other.email_addresses.get_n_items(); j++) { + var other_addr = (Geary.RFC822.MailboxAddress) other.email_addresses.get_item(j); if (this_addr.equal_to(other_addr)) { found = true; break; @@ -187,8 +187,8 @@ public class Application.Contact : Geary.BaseObject { Gee.Set email_addresses = new Gee.HashSet(); GLib.Value email_value = GLib.Value(typeof(Gee.Set)); - foreach (Geary.RFC822.MailboxAddress addr - in this.email_addresses) { + for (uint i = 0; i < this.email_addresses.get_n_items(); i++) { + var addr = (Geary.RFC822.MailboxAddress) this.email_addresses.get_item(i); email_addresses.add( new Folks.EmailFieldDetails(addr.address) ); @@ -279,9 +279,9 @@ public class Application.Contact : Geary.BaseObject { throws GLib.Error { ContactStore? store = this.store; if (store != null) { - Gee.Collection contacts = - new Gee.LinkedList(); - foreach (Geary.RFC822.MailboxAddress mailbox in this.email_addresses) { + var contacts = new Gee.LinkedList(); + for (uint i = 0; i < this.email_addresses.get_n_items(); i++) { + var mailbox = (Geary.RFC822.MailboxAddress) this.email_addresses.get_item(i); Geary.Contact? contact = yield store.lookup_engine_contact( mailbox, cancellable ); @@ -347,7 +347,9 @@ public class Application.Contact : Geary.BaseObject { private void update_from_engine() { Geary.RFC822.MailboxAddress mailbox = this.engine.get_rfc822_address(); - this._email_addresses = Geary.Collection.single(mailbox); + var addrs = new GLib.ListStore(typeof(Geary.RFC822.MailboxAddress)); + addrs.append(mailbox); + this._email_addresses = addrs; this.load_remote_resources = this.engine.flags.always_load_remote_images(); } diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index 1e682d6c..aeb78598 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -125,21 +125,13 @@ internal class Application.Controller : GLib.File config_dir = application.get_home_config_directory(); GLib.File data_dir = application.get_home_data_directory(); - // This initializes the IconFactory, important to do before - // the actions are created (as they refer to some of Geary's - // custom icons) - IconFactory.init(application.get_resource_directory()); - // Create DB upgrade dialog. this.database_manager = new DatabaseManager(application); // Initialise WebKit and WebViews Components.WebView.init_web_context( this.application.config, - this.application.get_web_extensions_dir(), - this.application.get_home_cache_directory().get_child( - "web-resources" - ) + this.application.get_web_extensions_dir() ); Components.WebView.load_resources(config_dir); Composer.WebView.load_resources(); @@ -406,7 +398,7 @@ internal class Application.Controller : // current window that is either a reply/forward for that // message, or there is a quote to insert into it. foreach (var existing in this.composer_widgets) { - if (existing.get_toplevel() == main && + if (existing.get_root() == main && (existing.current_mode == INLINE || existing.current_mode == INLINE_COMPACT) && existing.sender_context == send_context && @@ -957,6 +949,10 @@ internal class Application.Controller : } } + internal GLib.File get_web_cache_dir() { + return this.application.get_home_cache_directory().get_child("web-resources"); + } + /** Expunges removed accounts while the controller remains open. */ internal async void expunge_accounts() { try { @@ -1189,25 +1185,27 @@ internal class Application.Controller : context.authentication_prompting = false; } else { context.authentication_prompting = true; - PasswordDialog password_dialog = new PasswordDialog( + var password_dialog = new PasswordDialog( this.application.get_active_window(), account, service, credentials ); - if (password_dialog.run()) { + bool remember; + var password = yield password_dialog.get_password( + this.application.get_active_window(), + out remember + ); + if (password != null) { // The update the credentials for the service that the // credentials actually came from Geary.ServiceInformation creds_service = (credentials == account.incoming.credentials) ? account.incoming : account.outgoing; - creds_service.credentials = credentials.copy_with_token( - password_dialog.password - ); + creds_service.credentials = credentials.copy_with_token(password); // Update the remember password pref if changed - bool remember = password_dialog.remember_password; if (creds_service.remember_password != remember) { creds_service.remember_password = remember; account.changed(); @@ -1301,37 +1299,35 @@ internal class Application.Controller : // Returns true if the caller should try opening the account again private async bool account_database_error_async(Geary.Account account) { - bool retry = true; - // give the user two options: reset the Account local store, or exit Geary. A third // could be done to leave the Account in an unopened state, but we don't currently // have provisions for that. - QuestionDialog dialog = new QuestionDialog( - this.application.get_active_main_window(), + var dialog = new Adw.AlertDialog( _("Unable to open the database for %s").printf(account.information.id), - _("There was an error opening the local mail database for this account. This is possibly due to corruption of the database file in this directory:\n\n%s\n\nGeary can rebuild the database and re-synchronize with the server or exit.\n\nRebuilding the database will destroy all local email and its attachments. The mail on the your server will not be affected.") - .printf(account.information.data_dir.get_path()), - _("_Rebuild"), _("E_xit")); - dialog.use_secondary_markup(true); - switch (dialog.run()) { - case Gtk.ResponseType.OK: - // don't use Cancellable because we don't want to interrupt this process - try { - yield account.rebuild_async(); - } catch (Error err) { - ErrorDialog errdialog = new ErrorDialog( - this.application.get_active_main_window(), - _("Unable to rebuild database for “%s”").printf(account.information.id), - _("Error during rebuild:\n\n%s").printf(err.message)); - errdialog.run(); + null); + dialog.format_body_markup( + _("There was an error opening the local mail database for this account. This is possibly due to corruption of the database file in this directory:\n\n%s\n\nGeary can rebuild the database and re-synchronize with the server or exit.\n\nRebuilding the database will destroy all local email and its attachments. The mail on the your server will not be affected."), + account.information.data_dir.get_path()); + dialog.add_response("exit", _("E_xit")); + dialog.add_response("rebuild", _("_Rebuild")); - retry = false; - } - break; + string response = yield dialog.choose(this.application.get_active_main_window(), null); + if (response != "rebuild") { + return false; + } - default: - retry = false; - break; + // don't use Cancellable because we don't want to interrupt this process + bool retry = true; + try { + yield account.rebuild_async(); + } catch (Error err) { + var errdialog = new Adw.AlertDialog( + _("Unable to rebuild database for “%s”").printf(account.information.id), + _("Error during rebuild:\n\n%s").printf(err.message)); + errdialog.add_css_class("error"); + errdialog.present(this.application.get_active_main_window()); + + retry = false; } return retry; @@ -1454,10 +1450,11 @@ internal class Application.Controller : composer.present(); } - internal bool check_open_composers() { + internal async bool check_open_composers() { var do_quit = true; foreach (var composer in this.composer_widgets) { - if (composer.conditional_close(true, true) == CANCELLED) { + var status = yield composer.conditional_close(true, true); + if (status == CANCELLED) { do_quit = false; break; } @@ -1488,12 +1485,10 @@ internal class Application.Controller : Geary.Email sent) { /// Translators: The label for an in-app notification. string message = _("Email sent"); - Components.InAppNotification notification = - new Components.InAppNotification( - message, application.config.brief_notification_duration - ); + var toast = new Adw.Toast(message); + toast.timeout = application.config.brief_notification_duration; foreach (MainWindow window in this.application.get_main_windows()) { - window.add_notification(notification); + window.add_toast(toast); } AccountContext? context = this.accounts.get(service.account); diff --git a/src/client/application/application-database-manager.vala b/src/client/application/application-database-manager.vala index f559cbd9..197ff908 100644 --- a/src/client/application/application-database-manager.vala +++ b/src/client/application/application-database-manager.vala @@ -63,16 +63,10 @@ internal class Application.DatabaseManager : Geary.BaseObject { window.sensitive = false; } - var spinner = new Gtk.Spinner(); - spinner.set_size_request(45, 45); - spinner.start(); - - var grid = new Gtk.Grid(); - grid.orientation = VERTICAL; - grid.add(spinner); + var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); + box.append(new Adw.Spinner()); /// Translators: Label for account database upgrade dialog - grid.add(new Gtk.Label(_("Account update in progress"))); - grid.show_all(); + box.append(new Gtk.Label(_("Account update in progress"))); this.dialog = new Gtk.Dialog.with_buttons( /// Translators: Window title for account database upgrade @@ -81,15 +75,15 @@ internal class Application.DatabaseManager : Geary.BaseObject { this.application.get_active_main_window(), MODAL ); - this.dialog.get_style_context().add_class("geary-upgrade"); - this.dialog.get_content_area().add(grid); + this.dialog.add_css_class("geary-upgrade"); + this.dialog.get_content_area().append(box); this.dialog.deletable = false; - this.dialog.delete_event.connect(this.on_delete_event); + this.dialog.close_request.connect(on_close_request); this.dialog.close.connect(this.on_close); - this.dialog.show(); + this.dialog.present(); } - private bool on_delete_event() { + private bool on_close_request() { // Don't allow window to close until we're finished. return !this.monitor.is_in_progress; } diff --git a/src/client/application/application-main-window.vala b/src/client/application/application-main-window.vala index 9bdd8043..90b7862a 100644 --- a/src/client/application/application-main-window.vala +++ b/src/client/application/application-main-window.vala @@ -8,7 +8,7 @@ [GtkTemplate (ui = "/org/gnome/Geary/application-main-window.ui")] public class Application.MainWindow : - Hdy.ApplicationWindow, Geary.BaseInterface { + Adw.ApplicationWindow, Geary.BaseInterface { // Named actions. @@ -45,7 +45,6 @@ public class Application.MainWindow : { ACTION_FIND_IN_CONVERSATION, on_find_in_conversation_action }, { ACTION_SEARCH, on_search_activated }, { ACTION_SELECT_INBOX, on_select_inbox, "i" }, - { ACTION_NAVIGATION_BACK, go_to_previous_pane}, // Message actions { ACTION_REPLY_CONVERSATION, on_reply_conversation }, @@ -67,185 +66,10 @@ public class Application.MainWindow : { ACTION_ZOOM, on_zoom, "s" }, }; - // Handy leaflet children names - private const string INNER_LEAFLET = "inner_leaflet"; - private const string FOLDER_LIST = "folder_list"; - private const string CONVERSATION_LIST = "conversation_list"; - private const string CONVERSATION_VIEWER = "conversation_viewer"; - private const int UPDATE_UI_INTERVAL = 60; private const int MIN_CONVERSATION_COUNT = 50; - static construct { - // Set up default keybindings - unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class( - (ObjectClass) typeof(MainWindow).class_ref() - ); - - // - // Replying & forwarding - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.R, CONTROL_MASK, - "reply-conversation-sender", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.R, CONTROL_MASK | SHIFT_MASK, - "reply-conversation-all", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.L, CONTROL_MASK, - "forward-conversation", 0 - ); - - // Marking actions - // - // Unread is the primary action, so it doesn't get the - // modifier - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.U, CONTROL_MASK, - "mark-conversations-read", 1, typeof(bool), true - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.U, CONTROL_MASK | SHIFT_MASK, - "mark-conversations-read", 1, typeof(bool), false - ); - // Ephy uses Ctrl+D for bookmarking - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.D, CONTROL_MASK, - "mark-conversations-starred", 1, typeof(bool), true - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.D, CONTROL_MASK | SHIFT_MASK, - "mark-conversations-starred", 1, typeof(bool), false - ); - - // - // Moving & labelling - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.B, CONTROL_MASK, - "show-copy-menu", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.M, CONTROL_MASK, - "show-move-menu", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.K, CONTROL_MASK, - "archive-conversations", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.J, CONTROL_MASK, - "junk-conversations", 0 - ); - // Many ways to trash - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.BackSpace, 0, - "trash-conversations", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Delete, 0, - "trash-conversations", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.KP_Delete, 0, - "trash-conversations", 0 - ); - // Many ways to delete - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.BackSpace, SHIFT_MASK, - "delete-conversations", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Delete, SHIFT_MASK, - "delete-conversations", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.KP_Delete, SHIFT_MASK, - "delete-conversations", 0 - ); - - // - // Find & search - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.F, CONTROL_MASK, - "find", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.S, CONTROL_MASK, - "search", 0 - ); - - // - // Navigation - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Left, MOD1_MASK, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Back, 0, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Right, MOD1_MASK, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_RIGHT - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Forward, 0, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_RIGHT - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.comma, CONTROL_MASK, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_UP - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.period, CONTROL_MASK, - "navigate", 1, - typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_DOWN - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.Escape, 0, - "escape", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, - Gdk.Key.a, CONTROL_MASK, - "select_all", 0 - ); - } - - public static void add_accelerators(Client owner) { for (int i = 1; i <= 9; i++) { owner.add_window_accelerators( @@ -302,10 +126,9 @@ public class Application.MainWindow : public bool is_folder_list_shown { get { return ( - (!this.outer_leaflet.folded || - this.outer_leaflet.visible_child_name == INNER_LEAFLET) && - (!this.inner_leaflet.folded || - this.inner_leaflet.visible_child_name == FOLDER_LIST) + (!this.outer_view.collapsed || !this.outer_view.show_content) + && + (!this.inner_view.collapsed || !this.inner_view.show_content) ); } } @@ -314,10 +137,9 @@ public class Application.MainWindow : public bool is_conversation_list_shown { get { return ( - (!this.outer_leaflet.folded || - this.outer_leaflet.visible_child_name == INNER_LEAFLET) && - (!this.inner_leaflet.folded || - this.inner_leaflet.visible_child_name == CONVERSATION_LIST) + (!this.outer_view.collapsed || !this.outer_view.show_content) + && + (!this.inner_view.collapsed || this.inner_view.show_content) ); } } @@ -326,9 +148,9 @@ public class Application.MainWindow : public bool is_conversation_viewer_shown { get { return ( - (!this.outer_leaflet.folded || - this.outer_leaflet.visible_child_name == CONVERSATION_VIEWER) && - !this.has_composer + !this.outer_view.collapsed || this.outer_view.show_content + // XXX GTK4 not sure? + // && !this.has_composer ); } } @@ -356,7 +178,6 @@ public class Application.MainWindow : // Used to save/load the window state between sessions. public int window_width { get; set; } public int window_height { get; set; } - public bool window_maximized { get; set; } // Widget descendants public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); } @@ -390,27 +211,29 @@ public class Application.MainWindow : private Geary.TimeoutManager update_ui_timeout; private int64 update_ui_last = 0; - [GtkChild] private unowned Components.ApplicationHeaderBar application_headerbar; - [GtkChild] private unowned Components.ConversationListHeaderBar conversation_list_headerbar; - [GtkChild] public unowned Components.ConversationHeaderBar conversation_headerbar; + [GtkChild] private unowned Gtk.ToggleButton search_button; + [GtkChild] private unowned Gtk.ToggleButton conversation_list_selection_button; + [GtkChild] private unowned Gtk.MenuButton app_menu_button; + [GtkChild] private unowned MonitoredSpinner headerbar_spinner; - // Folds the inner leaftlet and conversation viewer - [GtkChild] private unowned Hdy.Leaflet outer_leaflet; + [GtkChild] private unowned Adw.HeaderBar conversation_list_headerbar; - // Folds the folder list and the conversation list - [GtkChild] private unowned Hdy.Leaflet inner_leaflet; + // Outer and inner split are [[ folder_box | conversation_list ] | conversation_viewer ] + [GtkChild] private unowned Adw.NavigationSplitView outer_view; + [GtkChild] private unowned Adw.NavigationSplitView inner_view; + [GtkChild] private unowned Adw.NavigationPage folder_list_page; [GtkChild] private unowned Gtk.ScrolledWindow folder_list_scrolled; + [GtkChild] private unowned Adw.NavigationPage conversation_list_page; [GtkChild] private unowned Gtk.Box conversation_list_box; [GtkChild] private unowned Gtk.Revealer conversation_list_actions_revealer; [GtkChild] private unowned Components.ConversationActions conversation_list_actions; [GtkChild] private unowned Components.ConversationActions conversation_viewer_actions; - [GtkChild] private unowned Gtk.Box conversation_viewer_box; - [GtkChild] private unowned Gtk.Revealer conversation_viewer_actions_revealer; + [GtkChild] private unowned Adw.NavigationPage conversation_viewer_page; - [GtkChild] private unowned Gtk.Overlay overlay; + [GtkChild] private unowned Adw.ToastOverlay overlay; [GtkChild] private unowned Components.InfoBarStack info_bars; @@ -511,12 +334,6 @@ public class Application.MainWindow : activate_action(get_window_action(ACTION_FIND_IN_CONVERSATION)); } - /** Keybinding signal for escaping current view. */ - [Signal (action=true)] - public virtual signal void escape() { - navigate_previous_pane(); - } - /** Keybinding signal for selecting all elements in current view. */ [Signal (action=true)] public virtual signal void select_all() { @@ -527,20 +344,6 @@ public class Application.MainWindow : [Signal (action=true)] public virtual signal void navigate(Gtk.ScrollType type) { switch (type) { - case Gtk.ScrollType.PAGE_LEFT: - if (get_direction() != RTL) { - go_to_previous_pane(); - } else { - go_to_next_pane(); - } - break; - case Gtk.ScrollType.PAGE_RIGHT: - if (get_direction() != RTL) { - go_to_next_pane(); - } else { - go_to_previous_pane(); - } - break; case Gtk.ScrollType.STEP_UP: activate_action(get_window_action(ACTION_CONVERSATION_UP)); break; @@ -577,12 +380,9 @@ public class Application.MainWindow : restore_saved_window_state(); if (Config.PROFILE != Client.PROFILE_RELEASE) { - this.get_style_context().add_class("devel"); + add_css_class("devel"); } - this.info_bars.shadow_type = IN; - this.conversation_list_info_bars.shadow_type = IN; - // Edit actions this.edit_actions.add_action_entries(EDIT_ACTIONS, this); insert_action_group(Action.Edit.GROUP_NAME, this.edit_actions); @@ -590,15 +390,6 @@ public class Application.MainWindow : // Window actions add_action_entries(MainWindow.WINDOW_ACTIONS, this); - this.focus_in_event.connect((w, e) => { - application.controller.window_focus_in(); - return false; - }); - this.focus_out_event.connect((w, e) => { - application.controller.window_focus_out(); - return false; - }); - setup_layout(application.config); update_command_actions(); @@ -646,7 +437,7 @@ public class Application.MainWindow : "Retry login, you will be prompted for your password" ); auth_retry.clicked.connect(on_auth_problem_retry); - this.auth_problem_infobar.get_action_area().add(auth_retry); + this.auth_problem_infobar.get_action_area().append(auth_retry); this.cert_problem_infobar = new Components.InfoBar( // Translators: An info bar status label @@ -662,7 +453,7 @@ public class Application.MainWindow : "Check the security details for the connection" ); cert_retry.clicked.connect(on_cert_problem_retry); - this.cert_problem_infobar.get_action_area().add(cert_retry); + this.cert_problem_infobar.get_action_area().append(cert_retry); this.map.connect(() => { this.folder_list.grab_focus(); @@ -671,34 +462,12 @@ public class Application.MainWindow : foreach (var actions in this.folder_conversation_actions) { actions.mark_message_button_toggled.connect(on_show_mark_menu); } - - Gtk.Settings.get_default().notify["gtk-decoration-layout"].connect( - on_gtk_decoration_layout_changed - ); - update_close_button_position(); } ~MainWindow() { base_unref(); } - /** {@inheritDoc} */ - public override void destroy() { - if (this.application != null) { - this.controller.account_available.disconnect( - on_account_available - ); - this.controller.account_unavailable.disconnect( - on_account_unavailable - ); - } - this.update_ui_timeout.reset(); - Gtk.Settings.get_default().notify["gtk-decoration-layout"].disconnect( - on_gtk_decoration_layout_changed - ); - base.destroy(); - } - /** Updates the window's title and headerbar titles. */ public void update_title() { AccountContext? account = get_selected_account_context(); @@ -719,8 +488,9 @@ public class Application.MainWindow : title = _("%s — %s").printf(folder_name, account_name); } this.title = title; - this.conversation_list_headerbar.account = account_name ?? ""; - this.conversation_list_headerbar.folder = folder_name?? ""; + this.conversation_list_page.title = account_name ?? ""; + //XXX GTK4 still need to figure out subtitles + // this.conversation_list_headerbar.subtitle = folder_name?? ""; } /** Updates the window's account status info bars. */ @@ -775,7 +545,7 @@ public class Application.MainWindow : this.folder_open.cancel(); var cancellable = this.folder_open = new GLib.Cancellable(); - this.conversation_list_headerbar.selection_open = false; + this.conversation_list_selection_button.active = false; // Dispose of all existing objects for the currently // selected model. @@ -855,6 +625,8 @@ public class Application.MainWindow : } } + this.outer_view.show_content = false; + this.inner_view.show_content = true; update_headerbar(); } @@ -867,7 +639,8 @@ public class Application.MainWindow : // The folder may have changed again by the type the async // call returns, so only continue if still current if (this.selected_folder == location) { - navigate_next_pane(); + this.outer_view.show_content = true; + // Since conversation ids don't persist between // conversation monitor instances, need to load // conversations based on their messages. @@ -902,7 +675,7 @@ public class Application.MainWindow : if (this.selected_folder == location) { var loaded = yield load_conversations_for_email(location, to_show); - navigate_next_pane(); + this.outer_view.show_content = true; if (loaded.size == 1) { // A single conversation was loaded, so ensure we // scroll to the email in the conversation. @@ -937,23 +710,15 @@ public class Application.MainWindow : /** Shows the appopriate window menu, if any. */ public void show_window_menu() { - if (this.outer_leaflet.folded) { - this.outer_leaflet.navigate(Hdy.NavigationDirection.BACK); - } - if (this.inner_leaflet.folded) { - this.inner_leaflet.navigate(Hdy.NavigationDirection.BACK); - } - this.application_headerbar.show_app_menu(); + this.outer_view.show_content = false; + this.inner_view.show_content = false; + this.app_menu_button.active = true; } /** Displays and focuses the search bar for the window. */ public void show_search_bar(string? text = null) { - if (!this.is_conversation_list_shown) { - if (this.outer_leaflet.folded) { - this.outer_leaflet.set_visible_child_name(INNER_LEAFLET); - } - this.inner_leaflet.set_visible_child_name(CONVERSATION_LIST); - } + this.outer_view.show_content = false; + this.inner_view.show_content = true; this.search_bar.grab_focus(); if (text != null) { @@ -999,8 +764,8 @@ public class Application.MainWindow : } else { this.conversation_viewer.do_compose(composer); } - // Show the correct leaflet - this.outer_leaflet.set_visible_child_name(CONVERSATION_VIEWER); + // Show the correct view + this.outer_view.show_content = true; } } @@ -1013,10 +778,11 @@ public class Application.MainWindow : internal bool close_composer(bool should_prompt, bool is_shutdown = false) { bool closed = true; Composer.Widget? composer = this.conversation_viewer.current_composer; - if (composer != null && - composer.conditional_close(should_prompt, is_shutdown) == CANCELLED) { - closed = false; - } + //XXX GTK4 + // if (composer != null && + // composer.conditional_close(should_prompt, is_shutdown) == CANCELLED) { + // closed = false; + // } return closed; } @@ -1235,75 +1001,60 @@ public class Application.MainWindow : // stays saved in the event of a crash. config.bind(Configuration.WINDOW_WIDTH_KEY, this, "window-width"); config.bind(Configuration.WINDOW_HEIGHT_KEY, this, "window-height"); - config.bind(Configuration.WINDOW_MAXIMIZE_KEY, this, "window-maximized"); + config.bind(Configuration.WINDOW_MAXIMIZE_KEY, this, "maximized"); } private void restore_saved_window_state() { Gdk.Display? display = Gdk.Display.get_default(); if (display != null) { - Gdk.Monitor? monitor = display.get_primary_monitor(); - if (monitor == null) { - monitor = display.get_monitor_at_point(1, 1); - } + Gdk.Monitor? monitor = null; + if (get_surface() != null) + monitor = display.get_monitor_at_surface(get_surface()); if (monitor != null && this.window_width <= monitor.geometry.width && this.window_height <= monitor.geometry.height) { set_default_size(this.window_width, this.window_height); } } - this.window_position = Gtk.WindowPosition.CENTER; - if (this.window_maximized) { + // XXX GTK4 - not sure if this is neede still + if (this.maximized) { maximize(); } } - // Called on [un]maximize and possibly others. Save maximized state - // for the next start. - public override bool window_state_event(Gdk.EventWindowState event) { - if ((event.new_window_state & Gdk.WindowState.WITHDRAWN) == 0) { - bool maximized = ( - (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0 + public override bool close_request() { + // XXX GTK4 check if this doesn't close too early + if (close_composer(true, false)) { + this.sensitive = false; + this.select_folder.begin( + null, + false, + true, + (obj, res) => { + this.select_folder.end(res); + destroy(); + } ); - if (this.window_maximized != maximized) { - this.window_maximized = maximized; - } } - return base.window_state_event(event); + + this.window_width = this.default_width; + this.window_height = this.default_height; + + if (this.application != null) { + this.controller.account_available.disconnect( + on_account_available + ); + this.controller.account_unavailable.disconnect( + on_account_unavailable + ); + } + this.update_ui_timeout.reset(); + + return base.close_request(); } - // Called on window resize. Save window size for the next start. - public override void size_allocate(Gtk.Allocation allocation) { - base.size_allocate(allocation); - - if (!this.window_maximized) { - Gdk.Display? display = get_display(); - Gdk.Window? window = get_window(); - if (display != null && window != null) { - Gdk.Monitor monitor = display.get_monitor_at_window(window); - - // Get the size via ::get_size instead of the - // allocation so that the window isn't ever-expanding. - int width = 0; - int height = 0; - get_size(out width, out height); - - // Only store if the values have changed and are - // reasonable-looking. - if (this.window_width != width && - width > 0 && width <= monitor.geometry.width) { - this.window_width = width; - } - if (this.window_height != height && - height > 0 && height <= monitor.geometry.height) { - this.window_height = height; - } - } - } - } - - public void add_notification(Components.InAppNotification notification) { - this.overlay.add_overlay(notification); - notification.show(); + public void add_toast(Adw.Toast toast) { + this.overlay.add_toast(toast); } private void setup_layout(Configuration config) { @@ -1312,7 +1063,7 @@ public class Application.MainWindow : // Search bar this.search_bar = new SearchBar(this.application.engine); this.search_bar.search_text_changed.connect(on_search); - this.conversation_list_box.pack_start(this.search_bar, false, false, 0); + this.conversation_list_box.append(this.search_bar); // Folder list @@ -1320,22 +1071,19 @@ public class Application.MainWindow : this.folder_list.move_conversation.connect(on_move_conversation); this.folder_list.copy_conversation.connect(on_copy_conversation); this.folder_list.folder_activated.connect(on_folder_activated); - this.folder_list_scrolled.add(this.folder_list); + this.folder_list_scrolled.child = this.folder_list; // Conversation list - this.conversation_list_box.pack_start( - this.conversation_list_info_bars, false, false, 0 - ); + this.conversation_list_box.append(this.conversation_list_info_bars); this.conversation_list_view = new ConversationList.View(this.application.config); this.conversation_list_view.mark_conversations.connect(on_mark_conversations); this.conversation_list_view.conversations_selected.connect(on_conversations_selected); this.conversation_list_view.conversation_activated.connect(on_conversation_activated); this.conversation_list_view.visible_conversations.notify.connect(on_visible_conversations_changed); + this.conversation_list_view.vexpand = true; - this.conversation_list_box.pack_start( - this.conversation_list_view, true, true, 0 - ); + this.conversation_list_box.append(this.conversation_list_view); // Conversation viewer this.conversation_viewer = new ConversationViewer( @@ -1346,51 +1094,47 @@ public class Application.MainWindow : ); this.conversation_viewer.hexpand = true; - this.conversation_viewer_box.add(this.conversation_viewer); + this.conversation_viewer_page.child = this.conversation_viewer; - this.conversation_list_headerbar.bind_property( - "search-open", - this.search_bar, "search-mode-enabled", - SYNC_CREATE | BIDIRECTIONAL - ); - this.conversation_list_headerbar.bind_property( - "selection-open", + this.conversation_list_selection_button.bind_property( + "active", this.conversation_list_view, "selection-mode-enabled", SYNC_CREATE | BIDIRECTIONAL ); - this.conversation_headerbar.bind_property( - "find-open", - this.conversation_viewer.conversation_find_bar, "search-mode-enabled", + this.search_button.bind_property( + "active", + this.search_bar, "search-mode-enabled", SYNC_CREATE | BIDIRECTIONAL ); - this.conversation_list_headerbar.notify["selection-open"].connect( - () => { + this.conversation_viewer.headerbar.bind_property( + "find-open", + this.search_bar, "search-mode-enabled", + SYNC_CREATE | BIDIRECTIONAL + ); + this.conversation_list_selection_button.notify["active"].connect( + (obj, pspec) => { + //XXX GTK4 pretty sure we can do this with breakpoints now + // Only show revealer with bottom actions in narrow mode if (this.conversation_list_view.selection_mode_enabled) this.conversation_list_actions_revealer.reveal_child = ( - this.outer_leaflet.folded); + this.inner_view.collapsed); else this.conversation_list_actions_revealer.reveal_child = false; } ); - this.conversation_headerbar.notify["shown-actions"].connect( - () => { - this.conversation_viewer_actions_revealer.reveal_child = ( - this.conversation_headerbar.shown_actions == - this.conversation_headerbar.compact_actions - ); - } - ); - this.application_headerbar.spinner.set_progress_monitor(progress_monitor); + this.headerbar_spinner.set_progress_monitor(progress_monitor); this.conversation_list_actions.set_mark_inverted(); - this.conversation_headerbar.full_actions.init(this.application.config); + this.conversation_viewer.headerbar.left_actions.init(this.application.config); + this.conversation_viewer.headerbar.right_actions.init(this.application.config); this.conversation_list_actions.init(this.application.config); this.conversation_viewer_actions.init(this.application.config); this.folder_conversation_actions = { - this.conversation_headerbar.full_actions, + this.conversation_viewer.headerbar.left_actions, + this.conversation_viewer.headerbar.right_actions, this.conversation_list_actions, this.conversation_viewer_actions }; @@ -1402,31 +1146,34 @@ public class Application.MainWindow : } } - /** {@inheritDoc} */ - public override bool key_press_event(Gdk.EventKey event) { - check_shift_event(event); - return base.key_press_event(event); + [GtkCallback] + private bool on_key_pressed(Gtk.EventControllerKey controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { + check_shift_event(keyval, true); + return false; } - /** {@inheritDoc} */ - public override bool key_release_event(Gdk.EventKey event) { - check_shift_event(event); - return base.key_release_event(event); + [GtkCallback] + private void on_key_released(uint keyval, uint keycode, Gdk.ModifierType state) { + check_shift_event(keyval, false); } - internal bool prompt_empty_folder(Geary.Folder.SpecialUse type) { + internal async bool prompt_empty_folder(Geary.Folder.SpecialUse type) { var folder_name = Util.I18n.to_folder_type_display_name(type); - ConfirmationDialog dialog = new ConfirmationDialog( - this, + var dialog = new Adw.AlertDialog( _("Empty all email from your %s folder?").printf(folder_name), _("This removes the email from Geary and your email server.") + - " " + _("This cannot be undone.") + "", - _("Empty %s").printf(folder_name), - "destructive-action" - ); - dialog.use_secondary_markup(true); - dialog.set_focus_response(Gtk.ResponseType.CANCEL); - return (dialog.run() == Gtk.ResponseType.OK); + " " + _("This cannot be undone.") + ""); + dialog.body_use_markup = true; + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("confirm", _("Empty %s").printf(folder_name)); + dialog.default_response = "cancel"; + dialog.close_response = "cancel"; + dialog.set_response_appearance("confirm", Adw.ResponseAppearance.DESTRUCTIVE); + string response = yield dialog.choose(this, null); + return (response == "confirm"); } /** Un-does the last executed application command, if any. */ @@ -1473,34 +1220,42 @@ public class Application.MainWindow : ); } - private bool prompt_delete_conversations(int count) { - ConfirmationDialog dialog = new ConfirmationDialog( - this, + private async bool prompt_delete_conversations(int count) { + var dialog = new Adw.AlertDialog( /// Translators: Primary text for a confirmation dialog ngettext( "Do you want to permanently delete this conversation?", "Do you want to permanently delete these conversations?", count ), - null, - _("Delete"), "destructive-action" + null ); - return (dialog.run() == Gtk.ResponseType.OK); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("delete", _("_Delete")); + dialog.default_response = "cancel"; + dialog.close_response = "cancel"; + dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE); + string response = yield dialog.choose(this, null); + return (response == "delete"); } - private bool prompt_delete_messages(int count) { - ConfirmationDialog dialog = new ConfirmationDialog( - this, + private async bool prompt_delete_messages(int count) { + var dialog = new Adw.AlertDialog( /// Translators: Primary text for a confirmation dialog ngettext( "Do you want to permanently delete this message?", "Do you want to permanently delete these messages?", count ), - null, - _("Delete"), "destructive-action" + null ); - return (dialog.run() == Gtk.ResponseType.OK); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("delete", _("_Delete")); + dialog.default_response = "cancel"; + dialog.close_response = "cancel"; + dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE); + string response = yield dialog.choose(this, null); + return (response == "delete"); } private async Gee.Collection @@ -1568,8 +1323,8 @@ public class Application.MainWindow : if (account != null) { this.conversation_list_actions.account = account; this.conversation_viewer_actions.account = account; - this.conversation_headerbar.full_actions.account = account; - this.conversation_headerbar.compact_actions.account = account; + this.conversation_viewer.headerbar.left_actions.account = account; + this.conversation_viewer.headerbar.right_actions.account = account; } update_command_actions(); @@ -1591,8 +1346,8 @@ public class Application.MainWindow : this.conversation_list_view.select_conversations(to_select); this.conversation_list_actions.selected_conversations = to_select.size; - this.conversation_headerbar.full_actions.selected_conversations = to_select.size; - this.conversation_headerbar.compact_actions.selected_conversations = to_select.size; + this.conversation_viewer.headerbar.right_actions.selected_conversations = to_select.size; + this.conversation_viewer.headerbar.left_actions.selected_conversations = to_select.size; if (this.selected_folder != null && !this.has_composer) { switch(to_select.size) { @@ -1718,7 +1473,8 @@ public class Application.MainWindow : } private void on_conversations_selected(Gee.Set selected) { - bool folded = this.outer_leaflet.folded; + bool folded = this.outer_view.collapsed; + // If folded, selection handled by activate if (selected.size > 1 || !folded) { select_conversations.begin(selected, Gee.Collection.empty(), true); @@ -1745,30 +1501,16 @@ public class Application.MainWindow : } } - private void update_close_button_position() { - bool at_end = Util.Gtk.close_button_at_end(); - - this.application_headerbar.show_close_button = ( - this.inner_leaflet.folded || !at_end - ); - this.conversation_list_headerbar.show_close_button = ( - this.inner_leaflet.folded || (at_end && this.outer_leaflet.folded) - ); - this.conversation_headerbar.show_close_button = ( - this.outer_leaflet.folded || at_end - ); - } - private void on_conversation_activated(Geary.App.Conversation activated, uint button) { - if (button == 1) { - bool folded = this.outer_leaflet.folded; - if (folded) { + if (button == Gdk.BUTTON_PRIMARY) { + bool collapsed = this.inner_view.collapsed; + this.outer_view.show_content = true; + if (collapsed) { Gee.Collection selected = new Gee.ArrayList(); selected.add(activated); select_conversations.begin(selected, Gee.Collection.empty(), true); } - go_to_next_pane(true); } else if (this.selected_folder != null) { if (this.selected_folder.used_as != DRAFTS) { this.application.new_window.begin( @@ -1833,9 +1575,10 @@ public class Application.MainWindow : } if (count > 0) { - this.conversation_list_headerbar.folder = _("%s (%d)").printf( - this.conversation_list_headerbar.folder, count - ); + //XXX GTK4 still need to figure out titles + // this.conversation_list_headerbar.subtitle = _("%s (%d)").printf( + // this.conversation_list_headerbar.subtitle, count + // ); } } } @@ -1848,7 +1591,7 @@ public class Application.MainWindow : sensitive && !multiple && this.is_conversation_viewer_shown ); get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(find_in_enabled); - this.conversation_headerbar.set_find_sensitive(find_in_enabled); + this.conversation_viewer.headerbar.set_find_sensitive(find_in_enabled); bool reply_sensitive = ( sensitive && @@ -1894,7 +1637,8 @@ public class Application.MainWindow : this.selected_folder_supports_trash ); this.conversation_list_actions.update_trash_button(show_trash); - this.conversation_headerbar.full_actions.update_trash_button(show_trash); + this.conversation_viewer.headerbar.left_actions.update_trash_button(show_trash); + this.conversation_viewer.headerbar.right_actions.update_trash_button(show_trash); } private async void update_context_dependent_actions(bool sensitive) { @@ -1945,15 +1689,16 @@ public class Application.MainWindow : update_trash_action(); } - private inline void check_shift_event(Gdk.EventKey event) { + private inline void check_shift_event(uint keyval, bool is_key_press) { + // XXX GTK4 - check if this is still needed // FIXME: it's possible the user will press two shift keys. We want // the shift key to report as released when they release ALL of them. // There doesn't seem to be an easy way to do this in Gdk. - if (event.keyval == Gdk.Key.Shift_L || event.keyval == Gdk.Key.Shift_R) { + if (keyval == Gdk.Key.Shift_L || keyval == Gdk.Key.Shift_R) { Gtk.Widget? focus = get_focus(); if (focus == null || (!(focus is Gtk.Entry) && !(focus is Composer.WebView))) { - set_shift_key_down(event.type == Gdk.EventType.KEY_PRESS); + set_shift_key_down(is_key_press); } } } @@ -1966,97 +1711,6 @@ public class Application.MainWindow : } } - private void navigate_next_pane() { - var focus = get_focus(); - if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) { - if (this.inner_leaflet.folded && - this.inner_leaflet.visible_child_name == FOLDER_LIST || - focus == this.folder_list) { - this.inner_leaflet.navigate(Hdy.NavigationDirection.FORWARD); - focus = this.conversation_list_view; - } else { - if (this.conversation_list_view.selected.size == 1 && - this.selected_folder.properties.email_total > 0) { - this.outer_leaflet.navigate(Hdy.NavigationDirection.FORWARD); - focus = this.conversation_viewer.visible_child; - } - } - } - focus_widget(focus); - } - - private void focus_next_pane() { - var focus = get_focus(); - if (focus != null) { - if (focus == this.folder_list || - focus.is_ancestor(this.folder_list)) { - focus = this.conversation_list_view; - } else if (focus == this.conversation_list_view || - focus.is_ancestor(this.conversation_list_view)) { - focus = this.conversation_viewer.visible_child; - } else if (focus == this.conversation_viewer || - focus.is_ancestor(this.conversation_viewer)) { - focus = this.folder_list; - } - } - focus_widget(focus); - } - - private void go_to_next_pane(bool only_if_folded=false) { - if (this.outer_leaflet.folded) { - navigate_next_pane(); - } else if (!only_if_folded) { - focus_next_pane(); - } - } - - private void navigate_previous_pane() { - var focus = get_focus(); - if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) { - if (this.inner_leaflet.folded) { - if (this.inner_leaflet.visible_child_name == CONVERSATION_LIST) { - this.inner_leaflet.navigate(Hdy.NavigationDirection.BACK); - focus = this.folder_list; - } - } else { - if (focus == this.conversation_list_view || - focus.is_ancestor(this.conversation_list_view)) - focus = this.folder_list; - else - focus = this.conversation_list_view; - } - } else { - this.outer_leaflet.navigate(Hdy.NavigationDirection.BACK); - focus = this.conversation_list_view; - } - focus_widget(focus); - } - - private void focus_previous_pane() { - var focus = get_focus(); - if (focus != null) { - if (focus == this.folder_list || - focus.is_ancestor(this.folder_list)) { - focus = this.conversation_viewer.visible_child; - } else if (focus == this.conversation_list_view || - focus.is_ancestor(this.conversation_list_view)) { - focus = this.folder_list; - } else if (focus == this.conversation_viewer || - focus.is_ancestor(this.conversation_viewer)) { - focus = this.conversation_list_view; - } - } - focus_widget(focus); - } - - private void go_to_previous_pane() { - if (this.outer_leaflet.folded) { - navigate_previous_pane(); - } else { - focus_previous_pane(); - } - } - private SimpleAction get_window_action(string name) { return (SimpleAction) lookup_action(name); } @@ -2074,9 +1728,9 @@ public class Application.MainWindow : } private void reply_conversation(Composer.Widget.ContextType context_type) { - if (this.outer_leaflet.folded) { + if (this.inner_view.collapsed) { this.conversation_list_view.activate_selected(); - navigate_next_pane(); + this.inner_view.show_content = true; // This is a lot of async actions, delay composer creation GLib.Timeout.add(500, () => { this.create_composer_from_viewer.begin(context_type); @@ -2091,7 +1745,7 @@ public class Application.MainWindow : // Done scanning. Check if we have enough messages to fill // the conversation list; if not, trigger a load_more(); Gtk.Scrollbar? scrollbar = ( - this.conversation_list_view.get_vscrollbar() as Gtk.Scrollbar + this.conversation_list_view.scrolled_window.get_vscrollbar() as Gtk.Scrollbar ); if (is_visible() && (scrollbar == null || !scrollbar.get_visible()) && @@ -2124,44 +1778,30 @@ public class Application.MainWindow : } [GtkCallback] - private bool on_focus_event() { - this.set_shift_key_down(false); - return false; + private void on_focus_enter(Gtk.EventControllerFocus controller) { + set_shift_key_down(false); + application.controller.window_focus_in(); } [GtkCallback] - private bool on_delete_event() { - if (close_composer(true, false)) { - this.sensitive = false; - this.select_folder.begin( - null, - false, - true, - (obj, res) => { - this.select_folder.end(res); - destroy(); - } - ); - } - return Gdk.EVENT_STOP; + private void on_focus_leave(Gtk.EventControllerFocus controller) { + set_shift_key_down(false); + application.controller.window_focus_out(); } [GtkCallback] - private void on_outer_leaflet_changed() { + private void on_inner_view_changed(GLib.Object object, ParamSpec pspec) { int selected = this.conversation_list_view.selected.size; update_conversation_actions( ConversationCount.for_size(selected) ); - update_close_button_position(); - if (this.outer_leaflet.folded) { + if (this.inner_view.collapsed) { // Ensure something useful gets the keyboard focus, given // GNOME/libhandy#179 if (this.is_conversation_list_shown) { this.conversation_list_view.grab_focus(); } else if (this.is_folder_list_shown) { this.folder_list.grab_focus(); - } else { - this.conversation_headerbar.back_button.visible = true; } // Close any open composer that is no longer visible @@ -2170,7 +1810,6 @@ public class Application.MainWindow : close_composer(false, false); } } else { - this.conversation_headerbar.back_button.visible = false; if (selected > 0) { select_conversations.begin( this.conversation_list_view.selected, @@ -2181,23 +1820,6 @@ public class Application.MainWindow : } } - [GtkCallback] - private void on_inner_leaflet_changed() { - update_close_button_position(); - if (this.inner_leaflet.folded) { - // Ensure something useful gets the keyboard focus, given - // GNOME/libhandy#179 - if (this.is_conversation_list_shown) { - this.conversation_list_headerbar.back_button.visible = true; - this.conversation_list_view.grab_focus(); - } else if (this.is_folder_list_shown) { - this.folder_list.grab_focus(); - } - } else { - this.conversation_list_headerbar.back_button.visible = false; - } - } - private void on_offline_infobar_response() { this.info_bars.remove(this.offline_infobar); } @@ -2304,27 +1926,22 @@ public class Application.MainWindow : } } if (command.undone_label != null) { - Components.InAppNotification ian = - new Components.InAppNotification(command.undone_label); - ian.set_button(_("Redo"), Action.Edit.prefix(Action.Edit.REDO)); - add_notification(ian); + var toast = new Adw.Toast(command.undone_label); + toast.button_label = _("Redo"); + toast.action_name = Action.Edit.prefix(Action.Edit.REDO); + add_toast(toast); } } private void on_command_redo(Command command) { update_command_actions(); if (command.executed_label != null) { - uint notification_time = - Components.InAppNotification.DEFAULT_DURATION; - if (command.executed_notification_brief) { - notification_time = - application.config.brief_notification_duration; - } - Components.InAppNotification ian = new Components.InAppNotification( - command.executed_label, notification_time - ); - ian.set_button(_("Undo"), Action.Edit.prefix(Action.Edit.UNDO)); - add_notification(ian); + var toast = new Adw.Toast(command.executed_label); + toast.button_label = _("Undo"); + toast.action_name = Action.Edit.prefix(Action.Edit.UNDO); + if (command.executed_notification_brief) + toast.timeout = application.config.brief_notification_duration; + add_toast(toast); } } @@ -2400,8 +2017,11 @@ public class Application.MainWindow : private void on_folder_activated(Geary.Folder? folder) { if (folder != null) { + this.inner_view.show_content = true; // Focus on conversation list will autoselect - go_to_next_pane(!this.application.config.autoselect); + if (this.application.config.autoselect) { + focus_widget(this.conversation_viewer.get_visible_child()); + } } } @@ -2453,7 +2073,8 @@ public class Application.MainWindow : this.conversation_list_actions_revealer.child_revealed) { this.conversation_list_actions.show_copy_menu(); } else if (this.is_conversation_viewer_shown) { - this.conversation_headerbar.shown_actions.show_copy_menu(); + this.conversation_viewer.headerbar.left_actions.show_copy_menu(); + this.conversation_viewer.headerbar.right_actions.show_copy_menu(); } else { error_bell(); } @@ -2711,23 +2332,27 @@ public class Application.MainWindow : } private void on_delete_conversation() { - Geary.FolderSupport.Remove target = - this.selected_folder as Geary.FolderSupport.Remove; + do_delete_conversation.begin(); + } + + private async void do_delete_conversation() { + var target = this.selected_folder as Geary.FolderSupport.Remove; + if (target == null) + return; + Gee.Collection conversations = this.conversation_list_view.selected; - if (target != null && this.prompt_delete_conversations(conversations.size)) { - this.controller.delete_conversations.begin( + if (!yield prompt_delete_conversations(conversations.size)) + return; + + try { + yield this.controller.delete_conversations( target, - conversations, - (obj, res) => { - try { - this.controller.delete_conversations.end(res); - } catch (GLib.Error err) { - handle_error(target.account.information, err); - } - } - ); + conversations); + } catch (GLib.Error err) { + handle_error(target.account.information, err); } + // No need to disable selection mode, handled by model change } @@ -2801,44 +2426,42 @@ public class Application.MainWindow : } private void on_email_trash(ConversationListBox view, Geary.Email target) { + do_trash_email.begin(view, target); + } + + private async void do_trash_email(ConversationListBox view, Geary.Email target) { Geary.Folder? source = this.selected_folder; - if (source != null) { - this.controller.move_messages_special.begin( + if (source == null) + return; + + try { + yield this.controller.move_messages_special( source, TRASH, Geary.Collection.single(view.conversation), - Geary.Collection.single(target.id), - (obj, res) => { - try { - this.controller.move_messages_special.end(res); - } catch (GLib.Error err) { - handle_error(source.account.information, err); - } - } - ); + Geary.Collection.single(target.id)); + } catch (GLib.Error err) { + handle_error(source.account.information, err); } } private void on_email_delete(ConversationListBox view, Geary.Email target) { - Geary.FolderSupport.Remove? source = - this.selected_folder as Geary.FolderSupport.Remove; - if (source != null && prompt_delete_messages(1)) { - this.controller.delete_messages.begin( - source, - Geary.Collection.single(view.conversation), - Geary.Collection.single(target.id), - (obj, res) => { - try { - this.controller.delete_messages.end(res); - } catch (GLib.Error err) { - handle_error(source.account.information, err); - } - } - ); - } + do_delete_email.begin(view, target); } - private void on_gtk_decoration_layout_changed() { - update_close_button_position(); + private async void do_delete_email(ConversationListBox view, Geary.Email target) { + Geary.FolderSupport.Remove? source = + this.selected_folder as Geary.FolderSupport.Remove; + if (source == null || !yield prompt_delete_messages(1)) + return; + + try { + yield this.controller.delete_messages( + source, + Geary.Collection.single(view.conversation), + Geary.Collection.single(target.id)); + } catch (GLib.Error err) { + handle_error(source.account.information, err); + } } } diff --git a/src/client/application/application-notification-plugin-context.vala b/src/client/application/application-notification-plugin-context.vala index 774a543f..cdee22e0 100644 --- a/src/client/application/application-notification-plugin-context.vala +++ b/src/client/application/application-notification-plugin-context.vala @@ -115,9 +115,9 @@ internal class Application.NotificationPluginContext : folder != null && this.folder_information.has_key(folder) && ( window == null || - !window.has_toplevel_focus || + !window.is_active || window.selected_folder != folder || - window.conversation_list_view.vadjustment.value > 0.0 + window.conversation_list_view.scrolled_window.vadjustment.value > 0.0 ) ); } diff --git a/src/client/application/application-plugin-manager.vala b/src/client/application/application-plugin-manager.vala index 178c0c54..6e110112 100644 --- a/src/client/application/application-plugin-manager.vala +++ b/src/client/application/application-plugin-manager.vala @@ -258,7 +258,7 @@ public class Application.PluginManager : GLib.Object { Geary.Folder? target = this.globals.folders.to_engine_folder(folder); if (target != null) { - if (!main.prompt_empty_folder(target.used_as)) { + if (!yield main.prompt_empty_folder(target.used_as)) { throw new Plugin.Error.PERMISSION_DENIED( "Permission not granted" ); @@ -419,7 +419,8 @@ public class Application.PluginManager : GLib.Object { public void insert_text(string plain_text) { var entry = this.backing.focused_input_widget as Gtk.Entry; if (entry != null) { - entry.insert_at_cursor(plain_text); + int position = entry.get_position(); + entry.insert_text(plain_text, plain_text.length, ref position); } else { this.backing.editor.body.insert_text(plain_text); } @@ -477,7 +478,7 @@ public class Application.PluginManager : GLib.Object { centre = new Gtk.Box(HORIZONTAL, 0); this.action_bar.set_center_widget(centre); } - centre.add(widget); + centre.append(widget); break; case END: @@ -487,7 +488,6 @@ public class Application.PluginManager : GLib.Object { } } - this.action_bar.show_all(); this.backing.editor.add_action_bar(this.action_bar); } @@ -513,26 +513,24 @@ public class Application.PluginManager : GLib.Object { if (item_type == typeof(Plugin.ActionBar.MenuItem)) { var menu_item = item as Plugin.ActionBar.MenuItem; - var label = new Gtk.Box(HORIZONTAL, 6); - label.add(new Gtk.Label(menu_item.label)); - label.add(new Gtk.Image.from_icon_name( - "pan-up-symbolic", Gtk.IconSize.BUTTON - )); - var button = new Gtk.MenuButton(); button.direction = Gtk.ArrowType.UP; - button.use_popover = true; button.menu_model = menu_item.menu; - button.add(label); + + var content = new Adw.ButtonContent(); + content.label = menu_item.label; + content.icon_name = "pan-up-symbolic"; + + button.child = content; return button; } if (item_type == typeof(Plugin.ActionBar.GroupItem)) { var group_items = item as Plugin.ActionBar.GroupItem; var box = new Gtk.Box(HORIZONTAL, 0); - box.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED); + box.add_css_class("linked"); foreach (var group_item in group_items.get_items()) { - box.add(widget_for_item(group_item)); + box.append(widget_for_item(group_item)); } return box; } diff --git a/src/client/components/components-attachment-pane.vala b/src/client/components/components-attachment-pane.vala index 6acff18c..b11d04c9 100644 --- a/src/client/components/components-attachment-pane.vala +++ b/src/client/components/components-attachment-pane.vala @@ -12,7 +12,7 @@ * shown will differ slightly based on which is selected. */ [GtkTemplate (ui = "/org/gnome/Geary/components-attachment-pane.ui")] -public class Components.AttachmentPane : Gtk.Grid { +public class Components.AttachmentPane : Gtk.Box { private const string GROUP_NAME = "cap"; @@ -36,24 +36,6 @@ public class Components.AttachmentPane : Gtk.Grid { { ACTION_SELECT_ALL, on_select_all }, }; - - // This exists purely to be able to set key bindings on it. - private class FlowBox : Gtk.FlowBox { - - /** Keyboard action to open the currently selected attachments. */ - [Signal (action=true)] - public signal void open_attachments(); - - /** Keyboard action to save the currently selected attachments. */ - [Signal (action=true)] - public signal void save_attachments(); - - /** Keyboard action to remove the currently selected attachments. */ - [Signal (action=true)] - public signal void remove_attachments(); - - } - // Displays an attachment's icon and details [GtkTemplate (ui = "/org/gnome/Geary/components-attachment-view.ui")] private class View : Gtk.Grid { @@ -112,7 +94,7 @@ public class Components.AttachmentPane : Gtk.Grid { return; } - Gdk.Pixbuf? pixbuf = null; + Gdk.Paintable? paintable = null; // XXX We need to hook up to GtkWidget::style-set and // reload the icon when the theme changes. @@ -131,26 +113,20 @@ public class Components.AttachmentPane : Gtk.Grid { Priority.DEFAULT, load_cancelled ); - pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async( + var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async( stream, preview_size, preview_size, true, load_cancelled ); pixbuf = pixbuf.apply_embedded_orientation(); + paintable = Gdk.Texture.for_pixbuf(pixbuf); } else { // Load the icon for this mime type GLib.Icon icon = GLib.ContentType.get_icon( this.gio_content_type ); - Gtk.IconTheme theme = Gtk.IconTheme.get_default(); - Gtk.IconLookupFlags flags = Gtk.IconLookupFlags.DIR_LTR; - if (get_direction() == Gtk.TextDirection.RTL) { - flags = Gtk.IconLookupFlags.DIR_RTL; - } - Gtk.IconInfo? icon_info = theme.lookup_by_gicon_for_scale( - icon, ATTACHMENT_ICON_SIZE, window_scale, flags + var theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); + paintable = theme.lookup_by_gicon( + icon, ATTACHMENT_ICON_SIZE, window_scale, get_direction(), 0 ); - if (icon_info != null) { - pixbuf = yield icon_info.load_icon_async(load_cancelled); - } } } catch (GLib.Error error) { debug("Failed to load icon for attachment '%s': %s", @@ -158,43 +134,14 @@ public class Components.AttachmentPane : Gtk.Grid { error.message); } - if (pixbuf != null) { - Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf( - pixbuf, window_scale, get_window() - ); - this.icon.set_from_surface(surface); + if (paintable != null) { + this.icon.paintable = paintable; } } } - static construct { - // Set up custom keybindings - unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class( - (ObjectClass) typeof(FlowBox).class_ref() - ); - - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.O, Gdk.ModifierType.CONTROL_MASK, "open-attachments", 0 - ); - - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.S, Gdk.ModifierType.CONTROL_MASK, "save-attachments", 0 - ); - - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.BackSpace, 0, "remove-attachments", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.Delete, 0, "remove-attachments", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.KP_Delete, 0, "remove-attachments", 0 - ); - } - - /** Determines if this pane's contents can be modified. */ public bool edit_mode { get; private set; } @@ -205,13 +152,13 @@ public class Components.AttachmentPane : Gtk.Grid { private GLib.SimpleActionGroup actions = new GLib.SimpleActionGroup(); - [GtkChild] private unowned Gtk.Grid attachments_container; + [GtkChild] private unowned Gtk.Box attachments_container; [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Button remove_button; - private FlowBox attachments_view; + private Gtk.FlowBox attachments_view; public AttachmentPane(bool edit_mode, @@ -225,22 +172,20 @@ public class Components.AttachmentPane : Gtk.Grid { this.manager = manager; - this.attachments_view = new FlowBox(); - this.attachments_view.open_attachments.connect(on_open_selected); - this.attachments_view.remove_attachments.connect(on_remove_selected); - this.attachments_view.save_attachments.connect(on_save_selected); + this.attachments_view = new Gtk.FlowBox(); + //XXX GTK4 need to check if shortcuts still work this.attachments_view.child_activated.connect(on_child_activated); this.attachments_view.selected_children_changed.connect(on_selected_changed); - this.attachments_view.button_press_event.connect(on_attachment_button_press); - this.attachments_view.popup_menu.connect(on_attachment_popup_menu); + Gtk.GestureClick gesture = new Gtk.GestureClick(); + gesture.pressed.connect(on_attachment_pressed); + this.attachments_view.add_controller(gesture); this.attachments_view.activate_on_single_click = false; this.attachments_view.max_children_per_line = 3; this.attachments_view.column_spacing = 6; this.attachments_view.row_spacing = 6; this.attachments_view.selection_mode = Gtk.SelectionMode.MULTIPLE; this.attachments_view.hexpand = true; - this.attachments_view.show(); - this.attachments_container.add(this.attachments_view); + this.attachments_container.append(this.attachments_view); this.actions.add_action_entries(action_entries, this); insert_action_group(GROUP_NAME, this.actions); @@ -249,7 +194,7 @@ public class Components.AttachmentPane : Gtk.Grid { public void add_attachment(Geary.Attachment attachment, GLib.Cancellable? cancellable) { View view = new View(attachment); - this.attachments_view.add(view); + this.attachments_view.append(view); this.attachments.add(attachment); view.load_icon.begin(cancellable); @@ -257,7 +202,7 @@ public class Components.AttachmentPane : Gtk.Grid { } public void open_attachment(Geary.Attachment attachment) { - open_attachments(Geary.Collection.single(attachment)); + open_attachments.begin(Geary.Collection.single(attachment)); } public void save_attachment(Geary.Attachment attachment) { @@ -270,12 +215,15 @@ public class Components.AttachmentPane : Gtk.Grid { public void remove_attachment(Geary.Attachment attachment) { this.attachments.remove(attachment); - this.attachments_view.foreach(child => { - Gtk.FlowBoxChild flow_child = (Gtk.FlowBoxChild) child; - if (((View) flow_child.get_child()).attachment == attachment) { - this.attachments_view.remove(child); - } - }); + for (int i = 0; true; i++) { + unowned var flow_child = this.attachments_view.get_child_at_index(i); + if (flow_child == null) + break; + if (((View) flow_child.get_child()).attachment == attachment) { + this.attachments_view.remove(flow_child); + i--; + } + } } public bool save_all() { @@ -317,7 +265,7 @@ public class Components.AttachmentPane : Gtk.Grid { bool ret = false; var selected = get_selected_attachments(); if (!selected.is_empty) { - open_attachments(selected); + open_attachments.begin(selected); ret = true; } return ret; @@ -362,29 +310,36 @@ public class Components.AttachmentPane : Gtk.Grid { set_action_enabled(ACTION_SELECT_ALL, len < this.attachments.size); } - private void open_attachments(Gee.Collection attachments) { - var main = this.get_toplevel() as Application.MainWindow; - if (main != null) { - Application.Client app = main.application; - bool confirmed = true; - if (app.config.ask_open_attachment) { - QuestionDialog ask_to_open = new QuestionDialog.with_checkbox( - main, - _("Are you sure you want to open these attachments?"), - _("Attachments may cause damage to your system if opened. Only open files from trusted sources."), - Stock._OPEN_BUTTON, Stock._CANCEL, _("Don’t _ask me again"), false - ); - if (ask_to_open.run() == Gtk.ResponseType.OK) { - app.config.ask_open_attachment = !ask_to_open.is_checked; - } else { - confirmed = false; - } - } + private async void open_attachments(Gee.Collection attachments) { + var main = get_root() as Application.MainWindow; + if (main == null) + return; - if (confirmed) { - foreach (var attachment in attachments) { - app.show_uri.begin(attachment.file.get_uri()); - } + Application.Client app = main.application; + if (app.config.ask_open_attachment) { + var dialog = new Adw.AlertDialog( + _("Are you sure you want to open these attachments?"), + _("Attachments may cause damage to your system if opened. Only open files from trusted sources.")); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("open", _("_Open")); + dialog.default_response = "open"; + dialog.close_response = "cancel"; + + var check = new Adw.SwitchRow(); + check.title = _("Don’t _ask me again"); + + string response = yield dialog.choose(main, null); + if (response != "open") + return; + app.config.ask_open_attachment = !check.active; + } + + foreach (var attachment in attachments) { + var launcher = new Gtk.FileLauncher(attachment.file); + try { + yield launcher.launch(get_native() as Gtk.Window, null); + } catch (GLib.Error err) { + warning("Couldn't show attachment: %s", err.message); } } } @@ -396,7 +351,7 @@ public class Components.AttachmentPane : Gtk.Grid { } } - private void show_popup(View view, Gdk.EventButton? event) { + private void show_popup(View view, Gdk.Rectangle? rect) { Gtk.Builder builder = new Gtk.Builder.from_resource( "/org/gnome/Geary/components-attachment-pane-menus.ui" ); @@ -410,21 +365,20 @@ public class Components.AttachmentPane : Gtk.Grid { GROUP_NAME, targets ); - Gtk.Menu menu = new Gtk.Menu.from_model(model); - menu.attach_to_widget(view, null); - if (event != null) { - menu.popup_at_pointer(event); - } else { - menu.popup_at_widget(view, CENTER, SOUTH, null); + Gtk.PopoverMenu menu = new Gtk.PopoverMenu.from_model(model); + menu.set_parent(view); + if (rect != null) { + menu.set_pointing_to(rect); } + menu.popup(); } private void beep() { - Gtk.Widget? toplevel = get_toplevel(); - if (toplevel == null) { - Gdk.Window? window = toplevel.get_window(); - if (window != null) { - window.beep(); + Gtk.Native? native = get_native(); + if (native == null) { + Gdk.Surface? surface = native.get_surface(); + if (surface != null) { + surface.beep(); } } } @@ -486,32 +440,19 @@ public class Components.AttachmentPane : Gtk.Grid { update_actions(); } - private bool on_attachment_popup_menu(Gtk.Widget widget) { - bool ret = Gdk.EVENT_PROPAGATE; - Gtk.Window parent = get_toplevel() as Gtk.Window; - if (parent != null) { - Gtk.FlowBoxChild? focus = parent.get_focus() as Gtk.FlowBoxChild; - if (focus != null && focus.parent == this.attachments_view) { - show_popup((View) focus.get_child(), null); - ret = Gdk.EVENT_STOP; - } - } - return ret; - } - - private bool on_attachment_button_press(Gtk.Widget widget, - Gdk.EventButton event) { - bool ret = Gdk.EVENT_PROPAGATE; - if (event.triggers_context_menu()) { + private void on_attachment_pressed(Gtk.GestureClick gesture, int n_press, double x, double y) { + var event = gesture.get_current_event(); + if (event.triggers_context_menu()) { Gtk.FlowBoxChild? child = this.attachments_view.get_child_at_pos( - (int) event.x, - (int) event.y + (int) x, + (int) y ); if (child != null) { - show_popup((View) child.get_child(), event); - ret = Gdk.EVENT_STOP; + Gdk.Rectangle rect = { (int) x, (int) y, 1, 1 }; + show_popup((View) child.get_child(), rect); + //XXX GTK4? + // ret = Gdk.EVENT_STOP; } - } - return ret; - } + } + } } diff --git a/src/client/components/components-conversation-actions.vala b/src/client/components/components-conversation-actions.vala index 122d59b6..7232f782 100644 --- a/src/client/components/components-conversation-actions.vala +++ b/src/client/components/components-conversation-actions.vala @@ -12,9 +12,25 @@ [GtkTemplate (ui = "/org/gnome/Geary/components-conversation-actions.ui")] public class Components.ConversationActions : Gtk.Box { - public bool show_conversation_actions { get; construct; } + public bool show_conversation_actions { + get { return this.action_buttons.visible; } + set { + if (this.action_buttons.visible == value) + return; + this.action_buttons.visible = value; + notify_property("show-conversation-actions"); + } + } - public bool show_response_actions { get; construct; } + public bool show_response_actions { + get { return this.response_buttons.visible; } + set { + if (this.response_buttons.visible == value) + return; + this.response_buttons.visible = value; + notify_property("show-conversation-actions"); + } + } public bool pack_justified { get; construct; } @@ -43,16 +59,12 @@ public class Components.ConversationActions : Gtk.Box { [GtkChild] private unowned Gtk.MenuButton mark_message_button { get; } [GtkChild] private unowned Gtk.MenuButton copy_message_button { get; } - [GtkChild] private unowned Gtk.Box action_buttons { get; } + [GtkChild] private unowned Gtk.Box action_buttons; [GtkChild] private unowned Gtk.Button archive_button; [GtkChild] private unowned Gtk.Button trash_delete_button; private bool show_trash_button = true; - // Load these at construction time - private Gtk.Image trash_image = new Gtk.Image.from_icon_name("user-trash-symbolic", Gtk.IconSize.MENU); - private Gtk.Image delete_image = new Gtk.Image.from_icon_name("edit-delete-symbolic", Gtk.IconSize.MENU); - static construct { set_css_name("components-conversation-actions"); } @@ -69,16 +81,13 @@ public class Components.ConversationActions : Gtk.Box { this.notify["selected-conversations"].connect(() => update_conversation_buttons()); this.notify["service-provider"].connect(() => update_conversation_buttons()); - this.mark_message_button.popover = new Gtk.Popover.from_model(null, mark_menu); + this.mark_message_button.menu_model = mark_menu; - this.mark_message_button.toggled.connect((button) => { + this.mark_message_button.activate.connect((button) => { if (button.active) mark_message_button_toggled(); }); - this.response_buttons.set_visible(this.show_response_actions); - this.action_buttons.set_visible(this.show_conversation_actions); - if (this.pack_justified) { this.action_buttons.hexpand = true; this.action_buttons.halign = END; @@ -102,14 +111,11 @@ public class Components.ConversationActions : Gtk.Box { } public void show_copy_menu() { - this.copy_message_button.clicked(); + this.copy_message_button.active = true; } public void set_mark_inverted() { - var image = new Gtk.Image.from_icon_name( - "pan-up-symbolic", Gtk.IconSize.BUTTON - ); - this.mark_message_button.set_image(image); + this.mark_message_button.icon_name = "pan-up-symbolic"; } public void update_trash_button(bool show_trash) { @@ -142,10 +148,7 @@ public class Components.ConversationActions : Gtk.Box { "Add label to conversations", this.selected_conversations ); - this.copy_message_button.set_image( - new Gtk.Image.from_icon_name( - "tag-symbolic", Gtk.IconSize.BUTTON) - ); + this.copy_message_button.icon_name = "tag-symbolic"; break; default: this.copy_message_button.tooltip_text = ngettext( @@ -153,10 +156,7 @@ public class Components.ConversationActions : Gtk.Box { "Copy conversations", this.selected_conversations ); - this.copy_message_button.set_image( - new Gtk.Image.from_icon_name( - "folder-symbolic", Gtk.IconSize.BUTTON) - ); + this.copy_message_button.icon_name = "folder-symbolic"; break; } } @@ -165,7 +165,7 @@ public class Components.ConversationActions : Gtk.Box { this.trash_delete_button.action_name = Action.Window.prefix( Application.MainWindow.ACTION_TRASH_CONVERSATION ); - this.trash_delete_button.image = trash_image; + this.trash_delete_button.icon_name = "user-trash-symbolic"; this.trash_delete_button.tooltip_text = ngettext( "Move conversation to Trash", "Move conversations to Trash", @@ -175,7 +175,7 @@ public class Components.ConversationActions : Gtk.Box { this.trash_delete_button.action_name = Action.Window.prefix( Application.MainWindow.ACTION_DELETE_CONVERSATION ); - this.trash_delete_button.image = delete_image; + this.trash_delete_button.icon_name = "edit-delete-symbolic"; this.trash_delete_button.tooltip_text = ngettext( "Delete conversation", "Delete conversations", diff --git a/src/client/components/components-entry-undo.vala b/src/client/components/components-entry-undo.vala index 53ff181c..f92960d1 100644 --- a/src/client/components/components-entry-undo.vala +++ b/src/client/components/components-entry-undo.vala @@ -6,7 +6,7 @@ */ /** - * Provides per-GTK Entry undo and redo using a command stack. + * Provides per-GTK Editable undo and redo using a command stack. */ public class Components.EntryUndo : Geary.BaseObject { @@ -84,13 +84,13 @@ public class Components.EntryUndo : Geary.BaseObject { } } - private void do_insert(Gtk.Entry target) { + private void do_insert(Gtk.Editable target) { int position = this.position; target.insert_text(this.text, -1, ref position); target.set_position(position); } - private void do_delete(Gtk.Entry target) { + private void do_delete(Gtk.Editable target) { target.delete_text( this.position, this.position + this.text.char_count() ); @@ -100,7 +100,7 @@ public class Components.EntryUndo : Geary.BaseObject { /** The entry being managed */ - public Gtk.Entry target { get; private set; } + public Gtk.Editable target { get; private set; } private Application.CommandStack commands; private EditType last_edit = NONE; @@ -113,7 +113,8 @@ public class Components.EntryUndo : Geary.BaseObject { private GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); - public EntryUndo(Gtk.Entry target) { + // XXX GTK4 maybe rename this to EditableUndo? + public EntryUndo(Gtk.Editable target) { this.edit_actions.add_action_entries(EDIT_ACTIONS, this); this.target = target; @@ -157,7 +158,7 @@ public class Components.EntryUndo : Geary.BaseObject { } ); while (!complete) { - Gtk.main_iteration(); + MainContext.default().iteration(true); } } @@ -179,7 +180,7 @@ public class Components.EntryUndo : Geary.BaseObject { } ); while (!complete) { - Gtk.main_iteration(); + MainContext.default().iteration(true); } } @@ -201,7 +202,7 @@ public class Components.EntryUndo : Geary.BaseObject { } ); while (!complete) { - Gtk.main_iteration(); + MainContext.default().iteration(true); } } @@ -298,7 +299,7 @@ public class Components.EntryUndo : Geary.BaseObject { private void on_deleted(int start, int end) { if (this.events_enabled) { // Normalise value of end to be something useful if needed - string text = this.target.buffer.get_text(); + string text = this.target.text; if (end < 0) { end = text.char_count(); } diff --git a/src/client/components/components-headerbar-application.vala b/src/client/components/components-headerbar-application.vala deleted file mode 100644 index 66bc9c29..00000000 --- a/src/client/components/components-headerbar-application.vala +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2017 Software Freedom Conservancy Inc. - * Copyright © 2021 Michael Gratton - * Copyright © 2022 Cédric Bellegarde - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - - -/** - * The Application HeaderBar - * - * @see Application.MainWindow - */ -[GtkTemplate (ui = "/org/gnome/Geary/components-headerbar-application.ui")] -public class Components.ApplicationHeaderBar : Hdy.HeaderBar { - - [GtkChild] private unowned Gtk.MenuButton app_menu_button; - [GtkChild] public unowned MonitoredSpinner spinner; - - - construct { - Gtk.Builder builder = new Gtk.Builder.from_resource("/org/gnome/Geary/components-menu-application.ui"); - MenuModel app_menu = (MenuModel) builder.get_object("app_menu"); - - this.app_menu_button.popover = new Gtk.Popover.from_model(null, app_menu); - } - - public void show_app_menu() { - this.app_menu_button.clicked(); - } - -} diff --git a/src/client/components/components-headerbar-conversation-list.vala b/src/client/components/components-headerbar-conversation-list.vala deleted file mode 100644 index 78f24d35..00000000 --- a/src/client/components/components-headerbar-conversation-list.vala +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2017 Software Freedom Conservancy Inc. - * Copyright © 2021 Michael Gratton - * Copyright © 2022 Cédric Bellegarde - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - - -/** - * The conversation list headerbar. - * - * @see Application.MainWindow - */ -[GtkTemplate (ui = "/org/gnome/Geary/components-headerbar-conversation-list.ui")] -public class Components.ConversationListHeaderBar : Hdy.HeaderBar { - - public string account { get; set; } - public string folder { get; set; } - public bool search_open { get; set; default = false; } - public bool selection_open { get; set; default = false; } - - [GtkChild] private unowned Gtk.ToggleButton search_button; - [GtkChild] private unowned Gtk.ToggleButton selection_button; - [GtkChild] public unowned Gtk.Button back_button; - - - construct { - this.bind_property("account", this, "title", BindingFlags.SYNC_CREATE); - this.bind_property("folder", this, "subtitle", BindingFlags.SYNC_CREATE); - - this.bind_property( - "search-open", - this.search_button, "active", - SYNC_CREATE | BIDIRECTIONAL - ); - this.bind_property( - "selection-open", - this.selection_button, "active", - SYNC_CREATE | BIDIRECTIONAL - ); - } -} diff --git a/src/client/components/components-headerbar-conversation.vala b/src/client/components/components-headerbar-conversation.vala index c6611b57..4db89e04 100644 --- a/src/client/components/components-headerbar-conversation.vala +++ b/src/client/components/components-headerbar-conversation.vala @@ -14,25 +14,23 @@ * @see Application.MainWindow */ [GtkTemplate (ui = "/org/gnome/Geary/components-headerbar-conversation.ui")] -public class Components.ConversationHeaderBar : Gtk.Bin { +public class Components.ConversationHeaderBar : Adw.Bin { public bool find_open { get; set; default = false; } - public ConversationActions shown_actions { - get { - return (ConversationActions) this.actions_squeezer.visible_child; - } - } + public bool compact { get; set; default = false; } - [GtkChild] private unowned Hdy.Squeezer actions_squeezer; - [GtkChild] public unowned ConversationActions full_actions; - [GtkChild] public unowned ConversationActions compact_actions; + [GtkChild] public unowned ConversationActions left_actions; + [GtkChild] public unowned ConversationActions right_actions; [GtkChild] private unowned Gtk.ToggleButton find_button; - [GtkChild] public unowned Gtk.Button back_button; - [GtkChild] private unowned Hdy.HeaderBar conversation_header; + [GtkChild] private unowned Adw.HeaderBar conversation_header; + // Keep a strong ref when it's temporarily removed + private Adw.HeaderBar? _conversation_header = null; + //XXX GTK4 need to figure out close buttons +#if 0 public bool show_close_button { get { return this.conversation_header.show_close_button; @@ -41,12 +39,9 @@ public class Components.ConversationHeaderBar : Gtk.Bin { this.conversation_header.show_close_button = value; } } +#endif construct { - this.actions_squeezer.notify["visible-child"].connect_after( - () => { notify_property("shown-actions"); } - ); - this.bind_property( "find-open", this.find_button, "active", @@ -54,17 +49,24 @@ public class Components.ConversationHeaderBar : Gtk.Bin { ); } - public void set_conversation_header(Hdy.HeaderBar header) { - remove(this.conversation_header); - header.hexpand = true; - header.show_close_button = this.conversation_header.show_close_button; - add(header); + public override void dispose() { + this._conversation_header = null; + base.dispose(); } - public void remove_conversation_header(Hdy.HeaderBar header) { - remove(header); - this.conversation_header.show_close_button = header.show_close_button; - add(this.conversation_header); + public void set_conversation_header(Adw.HeaderBar header) + requires (header.parent == null) { + this._conversation_header = null; + header.hexpand = true; + //XXX GTK4 need to figure out close buttons + // header.show_close_button = this.conversation_header.show_close_button; + this.child = header; + } + + public void remove_conversation_header(Adw.HeaderBar header) { + //XXX GTK4 need to figure out close buttons + // this.conversation_header.show_close_button = header.show_close_button; + this.child = this.conversation_header; } public void set_find_sensitive(bool is_sensitive) { diff --git a/src/client/components/components-in-app-notification.vala b/src/client/components/components-in-app-notification.vala deleted file mode 100644 index 044fd0c8..00000000 --- a/src/client/components/components-in-app-notification.vala +++ /dev/null @@ -1,75 +0,0 @@ -/* Copyright 2017 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/** - * Represents an in-app notification. - * - * Following the GNOME HIG, it should only contain a label and maybe a button. - * Looks like libadwaita toast, remove this when porting toward GTK4 - */ -[GtkTemplate (ui = "/org/gnome/Geary/components-in-app-notification.ui")] -public class Components.InAppNotification : Gtk.Revealer { - - /** Default length of time to show the notification. */ - public const uint DEFAULT_DURATION = 5; - - [GtkChild] private unowned Gtk.Label message_label; - - [GtkChild] private unowned Gtk.Button action_button; - - private uint duration; - - /** - * Creates an in-app notification. - * - * @param message The message that should be displayed. - * @param duration The length of time to show the notification, - * in seconds. - */ - public InAppNotification(string message, - uint duration = DEFAULT_DURATION) { - this.transition_type = Gtk.RevealerTransitionType.CROSSFADE; - this.message_label.label = message; - this.duration = duration; - } - - /** - * Sets a button for the notification. - */ - public void set_button(string label, string action_name) { - this.action_button.visible = true; - this.action_button.label = label; - this.action_button.action_name = action_name; - } - - public override void show() { - if (this.duration > 0) { - base.show(); - this.reveal_child = true; - - // Close after the given amount of time - GLib.Timeout.add_seconds( - this.duration, () => { close(); return false; } - ); - } - } - - /** - * Closes the in-app notification. - */ - [GtkCallback] - public void close() { - // Allows for the disappearing transition - this.reveal_child = false; - } - - // Make sure the notification gets destroyed after closing. - [GtkCallback] - private void on_child_revealed(Object src, ParamSpec p) { - if (!this.child_revealed) - destroy(); - } -} diff --git a/src/client/components/components-info-bar-stack.vala b/src/client/components/components-info-bar-stack.vala index f80a04f9..7becb0f4 100644 --- a/src/client/components/components-info-bar-stack.vala +++ b/src/client/components/components-info-bar-stack.vala @@ -158,7 +158,7 @@ public class Components.InfoBarStack : Gtk.Frame, Geary.BaseInterface { construct { - get_style_context().add_class("geary-info-bar-stack"); + add_css_class("geary-info-bar-stack"); update_queue_type(); } @@ -174,7 +174,7 @@ public class Components.InfoBarStack : Gtk.Frame, Geary.BaseInterface { * stack constructed, the info bar may or may not be revealed * immediately. */ - public new void add(Components.InfoBar to_add) { + public void add(Components.InfoBar to_add) { if (this.available.offer(to_add)) { update(); } @@ -187,7 +187,7 @@ public class Components.InfoBarStack : Gtk.Frame, Geary.BaseInterface { * replaced with the next info bar added. If the only info bar * present is removed, the stack also hides itself. */ - public new void remove(Components.InfoBar to_remove) { + public void remove(Components.InfoBar to_remove) { if (this.available.remove(to_remove)) { update(); } @@ -210,7 +210,7 @@ public class Components.InfoBarStack : Gtk.Frame, Geary.BaseInterface { // Not currently showing an info bar but have one to show, // so show it this.visible = true; - base.add(next); + this.child = next; next.revealed = true; } else if (current != null && next != current) { // Currently showing an info bar but should be showing @@ -241,7 +241,7 @@ public class Components.InfoBarStack : Gtk.Frame, Geary.BaseInterface { private void on_revealed(GLib.Object target, GLib.ParamSpec param) { var info_bar = target as Components.InfoBar; target.notify["revealed"].disconnect(on_revealed); - base.remove(info_bar); + this.child = null; remove(info_bar); } diff --git a/src/client/components/components-info-bar.vala b/src/client/components/components-info-bar.vala index 089712d7..f9616965 100644 --- a/src/client/components/components-info-bar.vala +++ b/src/client/components/components-info-bar.vala @@ -97,16 +97,13 @@ public class Components.InfoBar : Gtk.Box { this.description.tooltip_text = description; } - var container = new Gtk.Grid(); - container.orientation = VERTICAL; - container.valign = CENTER; - container.add(this.status); + var container = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + container.valign = Gtk.Align.CENTER; + container.append(this.status); if (this.description != null) { - container.add(this.description); + container.append(this.description); } - get_content_area().add(container); - - show_all(); + get_content_area().append(container); } public InfoBar.for_plugin(Plugin.InfoBar plugin, @@ -143,14 +140,12 @@ public class Components.InfoBar : Gtk.Box { var secondaries = plugin.secondary_buttons.bidir_list_iterator(); bool has_prev = secondaries.last(); while (has_prev) { - get_action_area().add(new_plugin_button(secondaries.get())); + get_action_area().append(new_plugin_button(secondaries.get())); has_prev = secondaries.previous(); } update_plugin_primary_button(); set_data(InfoBarStack.PRIORITY_QUEUE_KEY, priority); - - show_all(); } [GtkCallback] @@ -161,10 +156,9 @@ public class Components.InfoBar : Gtk.Box { response(Gtk.ResponseType.CLOSE); } - /* {@inheritDoc} */ - public override void destroy() { + public override void dispose() { this.plugin = null; - base.destroy(); + base.dispose(); } public Gtk.Box get_action_area() { @@ -180,7 +174,7 @@ public class Components.InfoBar : Gtk.Box { button.clicked.connect(() => { response(response_id); }); - get_action_area().add(button); + get_action_area().append(button); button.visible = true; return button; } @@ -194,7 +188,7 @@ public class Components.InfoBar : Gtk.Box { get_action_area().remove(plugin_primary_button); } if (new_button != null) { - get_action_area().add(new_button); + get_action_area().append(new_button); } this.plugin_primary_button = new_button; } @@ -204,11 +198,7 @@ public class Components.InfoBar : Gtk.Box { if (ui.icon_name == null) { button = new Gtk.Button.with_label(ui.label); } else { - var icon = new Gtk.Image.from_icon_name( - ui.icon_name, Gtk.IconSize.BUTTON - ); - button = new Gtk.Button(); - button.add(icon); + button = new Gtk.Button.from_icon_name(ui.icon_name); button.tooltip_text = ui.label; } button.set_action_name( @@ -217,26 +207,26 @@ public class Components.InfoBar : Gtk.Box { if (ui.action_target != null) { button.set_action_target_value(ui.action_target); } - button.show_all(); return button; } private void _set_message_type(Gtk.MessageType message_type) { if (this._message_type != message_type) { - Gtk.StyleContext context = this.get_style_context(); const string[] type_class = { - Gtk.STYLE_CLASS_INFO, - Gtk.STYLE_CLASS_WARNING, - Gtk.STYLE_CLASS_QUESTION, - Gtk.STYLE_CLASS_ERROR, + "info", + "warning", + "question", + "error", null }; if (type_class[this._message_type] != null) - context.remove_class(type_class[this._message_type]); + remove_css_class(type_class[this._message_type]); this._message_type = message_type; + // XXX GTK4 +#if 0 var atk_obj = this.get_accessible(); if (atk_obj is Atk.Object) { string name = null; @@ -271,9 +261,10 @@ public class Components.InfoBar : Gtk.Box { if (name != null) atk_obj.set_name(name); } + #endif if (type_class[this._message_type] != null) - context.add_class(type_class[this._message_type]); + add_css_class(type_class[this._message_type]); } } } diff --git a/src/client/components/components-inspector-error-view.vala b/src/client/components/components-inspector-error-view.vala index 568b5889..bbe90d23 100644 --- a/src/client/components/components-inspector-error-view.vala +++ b/src/client/components/components-inspector-error-view.vala @@ -9,7 +9,7 @@ * A view that displays information about an application error. */ [GtkTemplate (ui = "/org/gnome/Geary/components-inspector-error-view.ui")] -public class Components.InspectorErrorView : Gtk.Grid { +public class Components.InspectorErrorView : Adw.Bin { [GtkChild] private unowned Gtk.TextView problem_text; diff --git a/src/client/components/components-inspector-log-view.vala b/src/client/components/components-inspector-log-view.vala index c7f71feb..18b877f0 100644 --- a/src/client/components/components-inspector-log-view.vala +++ b/src/client/components/components-inspector-log-view.vala @@ -9,12 +9,7 @@ * A view that displays the contents of the Engine's log. */ [GtkTemplate (ui = "/org/gnome/Geary/components-inspector-log-view.ui")] -public class Components.InspectorLogView : Gtk.Grid { - - - private const int COL_MESSAGE = 0; - private const int COL_ACCOUNT = 1; - private const int COL_DOMAIN = 2; +public class Components.InspectorLogView : Gtk.Box { private class SidebarRow : Gtk.ListBoxRow { @@ -47,25 +42,56 @@ public class Components.InspectorLogView : Gtk.Grid { () => { notify_property("enabled"); } ); - var grid = new Gtk.Grid(); - grid.orientation = HORIZONTAL; - grid.add(label_widget); - grid.add(this.enabled_toggle); - add(grid); - - show_all(); + var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); + box.append(label_widget); + box.append(this.enabled_toggle); + this.child = box; } } + private class RecordRow : Gtk.Box { + + public Geary.Logging.Record? record { + get { return this._record; } + set { + this._record = value; + update(); + } + } + private Geary.Logging.Record? _record = null; + + private unowned Gtk.Label message_label; + + construct { + this.orientation = Gtk.Orientation.HORIZONTAL; + this.spacing = 6; + + var label = new Gtk.Label(""); + label.selectable = true; + label.add_css_class("monospace"); + append(label); + this.message_label = label; + } + + private void update() { + if (this.record == null) { + this.message_label.label = ""; + } else { + this.message_label.label = this.record.format(); + } + } + } + + /** Determines if the log record search user interface is shown. */ public bool search_mode_enabled { get { return this.search_bar.search_mode_enabled; } set { this.search_bar.search_mode_enabled = value; } } - [GtkChild] private unowned Hdy.SearchBar search_bar; + [GtkChild] private unowned Gtk.SearchBar search_bar; [GtkChild] private unowned Gtk.SearchEntry search_entry; @@ -73,18 +99,13 @@ public class Components.InspectorLogView : Gtk.Grid { [GtkChild] private unowned Gtk.ScrolledWindow logs_scroller; - [GtkChild] private unowned Gtk.TreeView logs_view; + [GtkChild] private unowned Gtk.ListView logs_view; - [GtkChild] private unowned Gtk.CellRendererText log_renderer; + [GtkChild] private unowned Gtk.MultiSelection selection; - private Gtk.ListStore logs_store = new Gtk.ListStore.newv({ - typeof(string), - typeof(string), - typeof(string) - }); - - private Gtk.TreeModelFilter logs_filter; + [GtkChild] private unowned GLib.ListStore logs_store; + [GtkChild] private unowned Gtk.CustomFilter logs_filter; private string[] logs_filter_terms = new string[0]; private bool update_logs = true; @@ -106,15 +127,7 @@ public class Components.InspectorLogView : Gtk.Grid { public signal void record_selection_changed(); - public InspectorLogView(Application.Configuration config, - Geary.AccountInformation? filter_by = null) { - GLib.Settings system = config.gnome_interface; - system.bind( - "monospace-font-name", - this.log_renderer, "font", - SettingsBindFlags.DEFAULT - ); - + public InspectorLogView(Geary.AccountInformation? filter_by = null) { // Prefill well-known engine logging domains add_domain(Geary.App.ConversationMonitor.LOGGING_DOMAIN); add_domain(Geary.Imap.ClientService.LOGGING_DOMAIN); @@ -127,6 +140,8 @@ public class Components.InspectorLogView : Gtk.Grid { this.search_bar.connect_entry(this.search_entry); this.sidebar.set_header_func(this.sidebar_header_update); this.account_filter = filter_by; + + this.logs_filter.set_filter_func(log_filter_func); } /** Loads log records from the logging system into the view. */ @@ -138,42 +153,29 @@ public class Components.InspectorLogView : Gtk.Grid { this.listener_installed = true; } - Gtk.ListStore logs_store = this.logs_store; Geary.Logging.Record? logs = first; int index = 0; while (logs != last) { - update_record(logs, logs_store, index++); + update_record(logs, this.logs_store, index++); logs = logs.next; } - - this.logs_filter = new Gtk.TreeModelFilter(this.logs_store, null); - this.logs_filter.set_visible_func(log_filter_func); - - this.logs_view.set_model(this.logs_filter); } /** Clears all log records from the view. */ public void clear() { - this.logs_store.clear(); + this.logs_store.remove_all(); this.first_pending = null; } - /** {@inheritDoc} */ - public override void destroy() { + ~InspectorLogView() { if (this.listener_installed) { Geary.Logging.set_log_listener(null); } - base.destroy(); - } - - /** Forwards a key press event to the search entry. */ - public bool handle_key_press(Gdk.EventKey event) { - return this.search_entry.key_press_event(event); } /** Returns the number of currently selected log records. */ - public int count_selected_records() { - return this.logs_view.get_selection().count_selected_rows(); + public uint count_selected_records() { + return (uint) this.selection.get_selection().get_size(); } /** Enables and disables updating log records as new ones arrive. */ @@ -204,33 +206,30 @@ public class Components.InspectorLogView : Gtk.Grid { out.put_string("```\n"); } string line_sep = format.get_line_separator(); - Gtk.TreeModel model = this.logs_view.model; + if (save_all) { // Save all rows selected - Gtk.TreeIter? iter; - bool valid = model.get_iter_first(out iter); - while (valid && !cancellable.is_cancelled()) { - save_record(model, iter, @out, cancellable); + for (uint i = 0; i < this.logs_store.get_n_items(); i++) { + if (cancellable.is_cancelled()) + break; + + var record = (Geary.Logging.Record) this.logs_store.get_item(i); + out.put_string(record.format()); out.put_string(line_sep); - valid = model.iter_next(ref iter); } } else { // Save only selected - GLib.Error? inner_err = null; - this.logs_view.get_selection().selected_foreach( - (model, path, iter) => { - if (inner_err == null) { - try { - save_record(model, iter, @out, cancellable); - out.put_string(line_sep); - } catch (GLib.Error err) { - inner_err = err; - } - } - } - ); - if (inner_err != null) { - throw inner_err; + Gtk.Bitset selected = this.selection.get_selection(); + for (uint i = 0; i < selected.get_size(); i++) { + if (cancellable.is_cancelled()) + break; + + uint position = selected.get_nth(i); + var record = (Geary.Logging.Record) this.logs_store.get_item(position); + assert(record != null); + + out.put_string(record.format()); + out.put_string(line_sep); } } if (format == MARKDOWN) { @@ -238,19 +237,6 @@ public class Components.InspectorLogView : Gtk.Grid { } } - private inline void save_record(Gtk.TreeModel model, - Gtk.TreeIter iter, - GLib.DataOutputStream @out, - GLib.Cancellable? cancellable) - throws GLib.Error { - GLib.Value value; - model.get_value(iter, COL_MESSAGE, out value); - string? message = (string) value; - if (message != null) { - out.put_string(message); - } - } - private void add_account(Geary.AccountInformation account) { if (this.seen_accounts.add(account.id)) { var row = new SidebarRow(ACCOUNT, account.display_name, account.id); @@ -311,11 +297,11 @@ public class Components.InspectorLogView : Gtk.Grid { string cleaned = Geary.String.reduce_whitespace(this.search_entry.text).casefold(); this.logs_filter_terms = cleaned.split(" "); - this.logs_filter.refilter(); + this.logs_filter.changed(Gtk.FilterChange.DIFFERENT); } private inline void update_record(Geary.Logging.Record record, - Gtk.ListStore store, + GLib.ListStore store, int position) { record.fill_well_known_sources(); if (record.account != null) { @@ -325,14 +311,7 @@ public class Components.InspectorLogView : Gtk.Grid { assert(record.format() != null); - var account = record.account; - store.insert_with_values( - null, - position, - COL_MESSAGE, record.format(), - COL_ACCOUNT, account != null ? account.information.id : "", - COL_DOMAIN, record.domain ?? "" - ); + store.insert(position, record); } private void sidebar_header_update(Gtk.ListBoxRow current_row, @@ -347,22 +326,19 @@ public class Components.InspectorLogView : Gtk.Grid { current_row.set_header(header); } - private bool log_filter_func(Gtk.TreeModel model, Gtk.TreeIter iter) { - GLib.Value value; - model.get_value(iter, COL_ACCOUNT, out value); - var account = (string) value; - var show_row = ( - account == "" || !(account in this.suppressed_accounts) + private bool log_filter_func(GLib.Object object) { + unowned var record = (Geary.Logging.Record) object; + + var account = record.account; + bool show_row = ( + account == null || !(account.information.id in this.suppressed_accounts) ); if (show_row) { - model.get_value(iter, COL_DOMAIN, out value); - var domain = (string) value; - show_row = !Geary.Logging.is_suppressed_domain(domain); + show_row = !Geary.Logging.is_suppressed_domain(record.domain ?? ""); } - model.get_value(iter, COL_MESSAGE, out value); - string message = (string) value; + string message = record.format(); if (show_row && this.logs_filter_terms.length > 0) { var folded_message = message.casefold(); foreach (string term in this.logs_filter_terms) { @@ -384,23 +360,34 @@ public class Components.InspectorLogView : Gtk.Grid { return show_row; } - [GtkCallback] - private void on_logs_size_allocate() { - if (this.autoscroll) { - update_scrollbar(); - } - } - [GtkCallback] private void on_logs_search_changed() { update_logs_filter(); } [GtkCallback] - private void on_logs_selection_changed() { + private void on_logs_selection_changed(Gtk.SelectionModel selection, + uint position, + uint changed) { record_selection_changed(); } + [GtkCallback] + private void on_item_factory_setup(Object object) { + unowned var item = (Gtk.ListItem) object; + + item.child = new RecordRow(); + } + + [GtkCallback] + private void on_item_factory_bind(Object object) { + unowned var item = (Gtk.ListItem) object; + unowned var record = (Geary.Logging.Record) item.item; + unowned var row = (RecordRow) item.child; + + row.record = record; + } + [GtkCallback] private void on_sidebar_row_activated(Gtk.ListBox list, Gtk.ListBoxRow activated) { diff --git a/src/client/components/components-inspector-system-view.vala b/src/client/components/components-inspector-system-view.vala index f61eed8a..dfaf3995 100644 --- a/src/client/components/components-inspector-system-view.vala +++ b/src/client/components/components-inspector-system-view.vala @@ -9,52 +9,7 @@ * A view that displays system and library information. */ [GtkTemplate (ui = "/org/gnome/Geary/components-inspector-system-view.ui")] -public class Components.InspectorSystemView : Gtk.Grid { - - - - private class DetailRow : Gtk.ListBoxRow { - - - private Gtk.Grid layout { - get; private set; default = new Gtk.Grid(); - } - - private Gtk.Label label { - get; private set; default = new Gtk.Label(""); - } - - private Gtk.Label value { - get; private set; default = new Gtk.Label(""); - } - - - public DetailRow(string label, string value) { - get_style_context().add_class("geary-labelled-row"); - - this.label.halign = Gtk.Align.START; - this.label.valign = Gtk.Align.CENTER; - this.label.set_text(label); - this.label.show(); - - this.value.halign = Gtk.Align.END; - this.value.hexpand = true; - this.value.valign = Gtk.Align.CENTER; - this.value.xalign = 1.0f; - this.value.set_text(value); - this.value.show(); - - this.layout.orientation = Gtk.Orientation.HORIZONTAL; - this.layout.add(this.label); - this.layout.add(this.value); - this.layout.show(); - add(this.layout); - - this.activatable = false; - show(); - } - - } +public class Components.InspectorSystemView : Gtk.Box { [GtkChild] private unowned Gtk.ListBox system_list; @@ -65,9 +20,11 @@ public class Components.InspectorSystemView : Gtk.Grid { public InspectorSystemView(Application.Client application) { this.details = application.get_runtime_information(); foreach (Application.Client.RuntimeDetail? detail in this.details) { - this.system_list.add( - new DetailRow("%s:".printf(detail.name), detail.value) - ); + var row = new Adw.ActionRow(); + row.add_css_class("property"); + row.title = detail.name; + row.subtitle = detail.value; + this.system_list.append(row); } } diff --git a/src/client/components/components-inspector.vala b/src/client/components/components-inspector.vala index e126dd1c..6dbaccc6 100644 --- a/src/client/components/components-inspector.vala +++ b/src/client/components/components-inspector.vala @@ -9,7 +9,7 @@ * A window that displays debugging and development information. */ [GtkTemplate (ui = "/org/gnome/Geary/components-inspector.ui")] -public class Components.Inspector : Gtk.ApplicationWindow { +public class Components.Inspector : Adw.ApplicationWindow { /** Determines the format used when serialising inspector data. */ @@ -68,7 +68,8 @@ public class Components.Inspector : Gtk.ApplicationWindow { public Inspector(Application.Client application) { Object(application: application); - this.title = this.header_bar.title = _("Inspector"); + //XXX GTK4 need to figure out titles + // this.title = this.header_bar.title = _("Inspector"); // Edit actions GLib.SimpleActionGroup edit_actions = new GLib.SimpleActionGroup(); @@ -78,7 +79,7 @@ public class Components.Inspector : Gtk.ApplicationWindow { // Window actions add_action_entries(WINDOW_ACTIONS, this); - this.log_pane = new InspectorLogView(application.config, null); + this.log_pane = new InspectorLogView(null); this.log_pane.record_selection_changed.connect( on_logs_selection_changed ); @@ -95,11 +96,15 @@ public class Components.Inspector : Gtk.ApplicationWindow { this.log_pane.load(Geary.Logging.get_earliest_record(), null); } - public override bool key_press_event(Gdk.EventKey event) { + [GtkCallback] + private bool on_key_pressed(Gtk.EventControllerKey controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { bool ret = Gdk.EVENT_PROPAGATE; if (this.log_pane.search_mode_enabled && - event.keyval == Gdk.Key.Escape) { + keyval == Gdk.Key.Escape) { // Manually deactivate search so the button stays in sync this.search_button.set_active(false); ret = Gdk.EVENT_STOP; @@ -109,18 +114,17 @@ public class Components.Inspector : Gtk.ApplicationWindow { this.log_pane.search_mode_enabled) { // Ensure and others are passed to the search // entry before getting used as an accelerator. - ret = this.log_pane.handle_key_press(event); + ret = controller.forward(this.log_pane); } - if (ret == Gdk.EVENT_PROPAGATE) { - ret = base.key_press_event(event); - } + return ret; + //XXX GTK4 - not sure how to handle this if (ret == Gdk.EVENT_PROPAGATE && !this.log_pane.search_mode_enabled) { // Nothing has handled the event yet, and search is not // active, so see if we want to activate it now. - ret = this.log_pane.handle_key_press(event); + ret = controller.forward(this.log_pane); if (ret == Gdk.EVENT_STOP) { this.search_button.set_active(true); } @@ -140,10 +144,9 @@ public class Components.Inspector : Gtk.ApplicationWindow { this.log_pane.enable_log_updates(enabled); } - private async void save(string path, + private async void save(GLib.File dest, GLib.Cancellable? cancellable) throws GLib.Error { - GLib.File dest = GLib.File.new_for_path(path); GLib.FileIOStream dest_io = yield dest.replace_readwrite_async( null, false, @@ -209,35 +212,29 @@ public class Components.Inspector : Gtk.ApplicationWindow { string clipboard_value = (string) bytes.get_data(); if (!Geary.String.is_empty(clipboard_value)) { - get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(clipboard_value, -1); + get_clipboard().set_text(clipboard_value); } } [GtkCallback] private void on_save_as_clicked() { - Gtk.FileChooserNative chooser = new Gtk.FileChooserNative( - _("Save As"), - this, - Gtk.FileChooserAction.SAVE, - _("Save As"), - _("Cancel") - ); - chooser.set_current_name( - new GLib.DateTime.now_local().format("Geary Inspector - %F %T.txt") - ); + save_as.begin((obj, res) => { + save_as.end(res); + }); + } - if (chooser.run() == Gtk.ResponseType.ACCEPT) { - this.save.begin( - chooser.get_filename(), - null, - (obj, res) => { - try { - this.save.end(res); - } catch (GLib.Error err) { - warning("Failed to save inspector data: %s", err.message); - } - } - ); + private async void save_as() { + var dialog = new Gtk.FileDialog(); + dialog.title = _("Save As"); + dialog.accept_label = _("Save As"); + dialog.initial_name = new DateTime.now_local().format("Geary Inspector - %F %T.txt"); + + try { + File? file = yield dialog.save(this, null); + if (file != null) + yield this.save(file, null); + } catch (Error err) { + warning("Failed to save inspector data: %s", err.message); } } diff --git a/src/client/components/components-placeholder-pane.vala b/src/client/components/components-placeholder-pane.vala index 2b82ec97..73870abc 100644 --- a/src/client/components/components-placeholder-pane.vala +++ b/src/client/components/components-placeholder-pane.vala @@ -9,7 +9,7 @@ * A placeholder image and message for empty views. */ [GtkTemplate (ui = "/org/gnome/Geary/components-placeholder-pane.ui")] -public class Components.PlaceholderPane : Gtk.Grid { +public class Components.PlaceholderPane : Gtk.Box { public const string CLASS_HAS_TEXT = "geary-has-text"; @@ -54,7 +54,7 @@ public class Components.PlaceholderPane : Gtk.Grid { this.subtitle_label.hide(); } if (this.title_label.visible || this.subtitle_label.visible) { - get_style_context().add_class(CLASS_HAS_TEXT); + add_css_class(CLASS_HAS_TEXT); } } diff --git a/src/client/components/components-preferences-dialog.vala b/src/client/components/components-preferences-dialog.vala new file mode 100644 index 00000000..b10b2d8b --- /dev/null +++ b/src/client/components/components-preferences-dialog.vala @@ -0,0 +1,159 @@ +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +[GtkTemplate (ui = "/org/gnome/Geary/components-preferences-dialog.ui")] +public class Components.PreferencesDialog : Adw.PreferencesDialog { + + [GtkChild] private unowned Adw.SwitchRow autoselect_row; + [GtkChild] private unowned Adw.SwitchRow display_preview_row; + [GtkChild] private unowned Adw.SwitchRow single_key_shortcuts_row; + [GtkChild] private unowned Adw.SwitchRow startup_notifications_row; + [GtkChild] private unowned Adw.SwitchRow trust_images_row; + + [GtkChild] private unowned Adw.PreferencesGroup plugins_group; + + private class PluginRow : Adw.ActionRow { + + private Peas.PluginInfo plugin; + private Application.PluginManager plugins; + private Gtk.Switch sw = new Gtk.Switch(); + + + public PluginRow(Peas.PluginInfo plugin, + Application.PluginManager plugins) { + this.plugin = plugin; + this.plugins = plugins; + + this.sw.active = plugin.is_loaded(); + this.sw.notify["active"].connect_after(() => update_plugin()); + this.sw.valign = CENTER; + + this.title = plugin.get_name(); + this.subtitle = plugin.get_description(); + this.activatable_widget = this.sw; + this.add_suffix(this.sw); + + plugins.plugin_activated.connect((info) => { + if (this.plugin == info) { + this.sw.active = true; + } + }); + plugins.plugin_deactivated.connect((info) => { + if (this.plugin == info) { + this.sw.active = false; + } + }); + plugins.plugin_error.connect((info) => { + if (this.plugin == info) { + this.sw.active = false; + this.sw.sensitive = false; + } + }); + } + + private void update_plugin() { + if (this.sw.active && !this.plugin.is_loaded()) { + bool loaded = false; + try { + loaded = this.plugins.load_optional(this.plugin); + } catch (GLib.Error err) { + warning( + "Plugin %s not able to be loaded: %s", + plugin.get_name(), err.message + ); + } + if (!loaded) { + this.sw.active = false; + } + } else if (!sw.active && this.plugin.is_loaded()) { + bool unloaded = false; + try { + unloaded = this.plugins.unload_optional(this.plugin); + } catch (GLib.Error err) { + warning( + "Plugin %s not able to be loaded: %s", + plugin.get_name(), err.message + ); + } + if (!unloaded) { + this.sw.active = true; + } + } + } + + } + + + + /** Returns the window's associated client application instance. */ + public Application.Client? application { get; construct set; } + + private Application.PluginManager plugins; + + + public PreferencesDialog(Application.Client application, + Application.PluginManager plugins) { + Object(application: application); + this.plugins = plugins; + + setup_general_pane(); + setup_plugin_pane(); + } + + private void setup_general_pane() { + Application.Configuration config = this.application.config; + config.bind( + Application.Configuration.AUTOSELECT_KEY, + this.autoselect_row, + "active" + ); + config.bind( + Application.Configuration.DISPLAY_PREVIEW_KEY, + this.display_preview_row, + "active" + ); + config.bind( + Application.Configuration.SINGLE_KEY_SHORTCUTS, + this.single_key_shortcuts_row, + "active" + ); + config.bind( + Application.Configuration.RUN_IN_BACKGROUND_KEY, + this.startup_notifications_row, + "active" + ); + config.bind_with_mapping( + Application.Configuration.IMAGES_TRUSTED_DOMAINS, + this.trust_images_row, + "active", + (GLib.SettingsBindGetMappingShared) settings_trust_images_getter, + (GLib.SettingsBindSetMappingShared) settings_trust_images_setter + ); + } + + private void setup_plugin_pane() { + foreach (Peas.PluginInfo plugin in + this.plugins.get_optional_plugins()) { + this.plugins_group.add(new PluginRow(plugin, this.plugins)); + } + } + + private static bool settings_trust_images_getter(GLib.Value value, GLib.Variant variant, void* user_data) { + var domains = variant.get_strv(); + value.set_boolean(domains.length > 0 && domains[0] == "*"); + return true; + } + + private static GLib.Variant settings_trust_images_setter(GLib.Value value, GLib.VariantType expected_type, void* user_data) { + var trusted = value.get_boolean(); + string[] values = {}; + if (trusted) + values += "*"; + return new GLib.Variant.strv(values); + } +} diff --git a/src/client/components/components-preferences-window.vala b/src/client/components/components-preferences-window.vala deleted file mode 100644 index 3eff17db..00000000 --- a/src/client/components/components-preferences-window.vala +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2016 Software Freedom Conservancy Inc. - * Copyright 2019 Michael Gratton - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -public class Components.PreferencesWindow : Hdy.PreferencesWindow { - - - private const string ACTION_CLOSE = "preferences-close"; - - private const ActionEntry[] WINDOW_ACTIONS = { - { Action.Window.CLOSE, on_close }, - { ACTION_CLOSE, on_close }, - }; - - private class PluginRow : Hdy.ActionRow { - - private Peas.PluginInfo plugin; - private Application.PluginManager plugins; - private Gtk.Switch sw = new Gtk.Switch(); - - - public PluginRow(Peas.PluginInfo plugin, - Application.PluginManager plugins) { - this.plugin = plugin; - this.plugins = plugins; - - this.sw.active = plugin.is_loaded(); - this.sw.notify["active"].connect_after(() => update_plugin()); - this.sw.valign = CENTER; - - this.title = plugin.get_name(); - this.subtitle = plugin.get_description(); - this.activatable_widget = this.sw; - this.add(this.sw); - - plugins.plugin_activated.connect((info) => { - if (this.plugin == info) { - this.sw.active = true; - } - }); - plugins.plugin_deactivated.connect((info) => { - if (this.plugin == info) { - this.sw.active = false; - } - }); - plugins.plugin_error.connect((info) => { - if (this.plugin == info) { - this.sw.active = false; - this.sw.sensitive = false; - } - }); - } - - private void update_plugin() { - if (this.sw.active && !this.plugin.is_loaded()) { - bool loaded = false; - try { - loaded = this.plugins.load_optional(this.plugin); - } catch (GLib.Error err) { - warning( - "Plugin %s not able to be loaded: %s", - plugin.get_name(), err.message - ); - } - if (!loaded) { - this.sw.active = false; - } - } else if (!sw.active && this.plugin.is_loaded()) { - bool unloaded = false; - try { - unloaded = this.plugins.unload_optional(this.plugin); - } catch (GLib.Error err) { - warning( - "Plugin %s not able to be loaded: %s", - plugin.get_name(), err.message - ); - } - if (!unloaded) { - this.sw.active = true; - } - } - } - - } - - - public static void add_accelerators(Application.Client app) { - app.add_window_accelerators(ACTION_CLOSE, { "Escape" } ); - } - - - /** Returns the window's associated client application instance. */ - public new Application.Client? application { - get { return (Application.Client) base.get_application(); } - set { base.set_application(value); } - } - - private Application.PluginManager plugins; - - - public PreferencesWindow(Application.MainWindow parent, - Application.PluginManager plugins) { - Object( - application: parent.application, - default_width: 800, - default_height: 600, - transient_for: parent - ); - this.plugins = plugins; - - add_general_pane(); - add_plugin_pane(); - } - - private void add_general_pane() { - var autoselect = new Gtk.Switch(); - autoselect.valign = CENTER; - - var autoselect_row = new Hdy.ActionRow(); - /// Translators: Preferences label - autoselect_row.title = _("_Automatically select next message"); - autoselect_row.use_underline = true; - autoselect_row.activatable_widget = autoselect; - autoselect_row.add(autoselect); - - var display_preview = new Gtk.Switch(); - display_preview.valign = CENTER; - - var display_preview_row = new Hdy.ActionRow(); - /// Translators: Preferences label - display_preview_row.title = _("_Display conversation preview"); - display_preview_row.use_underline = true; - display_preview_row.activatable_widget = display_preview; - display_preview_row.add(display_preview); - - var single_key_shortucts = new Gtk.Switch(); - single_key_shortucts.valign = CENTER; - - var single_key_shortucts_row = new Hdy.ActionRow(); - /// Translators: Preferences label - single_key_shortucts_row.title = _("Use _single key email shortcuts"); - single_key_shortucts_row.tooltip_text = _( - "Enable keyboard shortcuts for email actions that do not require pressing " - ); - single_key_shortucts_row.use_underline = true; - single_key_shortucts_row.activatable_widget = single_key_shortucts; - single_key_shortucts_row.add(single_key_shortucts); - - var startup_notifications = new Gtk.Switch(); - startup_notifications.valign = CENTER; - - var startup_notifications_row = new Hdy.ActionRow(); - /// Translators: Preferences label - startup_notifications_row.title = _("_Watch for new mail when closed"); - startup_notifications_row.use_underline = true; - /// Translators: Preferences tooltip - startup_notifications_row.tooltip_text = _( - "Geary will keep running after all windows are closed" - ); - startup_notifications_row.activatable_widget = startup_notifications; - startup_notifications_row.add(startup_notifications); - - var trust_images = new Gtk.Switch(); - trust_images.valign = CENTER; - - var trust_images_row = new Hdy.ActionRow(); - /// Translators: Preferences label - trust_images_row.title = _("_Always load images"); - trust_images_row.subtitle = _("Showing remote images allows the sender to track you"); - trust_images_row.use_underline = true; - trust_images_row.activatable_widget = trust_images; - trust_images_row.add(trust_images); - - var unset_html_colors = new Gtk.Switch(); - unset_html_colors.valign = CENTER; - - var unset_html_colors_row = new Hdy.ActionRow(); - /// Translators: Preferences label - unset_html_colors_row.title = _("_Override the original colors in HTML emails"); - unset_html_colors_row.subtitle = _("Overrides the original colors in HTML messages to integrate better with the app theme. Requires restart."); - unset_html_colors_row.use_underline = true; - unset_html_colors_row.activatable_widget = unset_html_colors; - unset_html_colors_row.add(unset_html_colors); - - var group = new Hdy.PreferencesGroup(); - /// Translators: Preferences group title - //group.title = _("General"); - /// Translators: Preferences group description - //group.description = _("General application preferences"); - group.add(autoselect_row); - group.add(display_preview_row); - group.add(single_key_shortucts_row); - group.add(startup_notifications_row); - group.add(trust_images_row); - group.add(unset_html_colors_row); - - var page = new Hdy.PreferencesPage(); - /// Translators: Preferences page title - page.title = _("Preferences"); - page.icon_name = "preferences-other-symbolic"; - page.add(group); - page.show_all(); - - add(page); - - GLib.SimpleActionGroup window_actions = new GLib.SimpleActionGroup(); - window_actions.add_action_entries(WINDOW_ACTIONS, this); - insert_action_group(Action.Window.GROUP_NAME, window_actions); - - Application.Client? application = this.application; - if (application != null) { - Application.Configuration config = application.config; - config.bind( - Application.Configuration.AUTOSELECT_KEY, - autoselect, - "state" - ); - config.bind( - Application.Configuration.DISPLAY_PREVIEW_KEY, - display_preview, - "state" - ); - config.bind( - Application.Configuration.SINGLE_KEY_SHORTCUTS, - single_key_shortucts, - "state" - ); - config.bind( - Application.Configuration.RUN_IN_BACKGROUND_KEY, - startup_notifications, - "state" - ); - config.bind_with_mapping( - Application.Configuration.IMAGES_TRUSTED_DOMAINS, - trust_images, - "state", - (GLib.SettingsBindGetMappingShared) settings_trust_images_getter, - (GLib.SettingsBindSetMappingShared) settings_trust_images_setter - ); - config.bind( - Application.Configuration.UNSET_HTML_COLORS, - unset_html_colors, - "state" - ); - } - } - - private void add_plugin_pane() { - var group = new Hdy.PreferencesGroup(); - /// Translators: Preferences group title - //group.title = _("Plugins"); - /// Translators: Preferences group description - //group.description = _("Optional features for Geary"); - - Application.Client? application = this.application; - if (application != null) { - foreach (Peas.PluginInfo plugin in - this.plugins.get_optional_plugins()) { - group.add(new PluginRow(plugin, this.plugins)); - } - } - - var page = new Hdy.PreferencesPage(); - /// Translators: Preferences page title - page.title = _("Plugins"); - page.icon_name = "application-x-addon-symbolic"; - page.add(group); - page.show_all(); - - add(page); - } - - private void on_close() { - close(); - } - - private static bool settings_trust_images_getter(GLib.Value value, GLib.Variant variant, void* user_data) { - var domains = variant.get_strv(); - value.set_boolean(domains.length > 0 && domains[0] == "*"); - return true; - } - - private static GLib.Variant settings_trust_images_setter(GLib.Value value, GLib.VariantType expected_type, void* user_data) { - var trusted = value.get_boolean(); - string[] values = {}; - if (trusted) - values += "*"; - return new GLib.Variant.strv(values); - } -} diff --git a/src/client/components/components-problem-report-info-bar.vala b/src/client/components/components-problem-report-info-bar.vala index d2042f8c..9fb1cd4b 100644 --- a/src/client/components/components-problem-report-info-bar.vala +++ b/src/client/components/components-problem-report-info-bar.vala @@ -106,14 +106,13 @@ public class Components.ProblemReportInfoBar : InfoBar { } private void show_details() { - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { var dialog = new Dialogs.ProblemDetailsDialog( - main, main.application, this.report ); - dialog.show(); + dialog.present(main); } } diff --git a/src/client/components/components-reflow-box.c b/src/client/components/components-reflow-box.c index eee54418..d18a089f 100644 --- a/src/client/components/components-reflow-box.c +++ b/src/client/components/components-reflow-box.c @@ -10,6 +10,8 @@ #include +// XXX GTK4 - I really need to know how this works before reimplementing it +#if 0 #define COMPONENTS_TYPE_REFLOW_BOX (components_reflow_box_get_type()) G_DECLARE_FINAL_TYPE (ComponentsReflowBox, components_reflow_box, COMPONENTS, REFLOW_BOX, GtkContainer) @@ -491,6 +493,4 @@ components_reflow_box_new (void) { return g_object_new (COMPONENTS_TYPE_REFLOW_BOX, NULL); } - - - +#endif diff --git a/src/client/components/components-search-bar.vala b/src/client/components/components-search-bar.vala index 3a4e09e7..9ee9cb1e 100644 --- a/src/client/components/components-search-bar.vala +++ b/src/client/components/components-search-bar.vala @@ -6,11 +6,17 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class SearchBar : Hdy.SearchBar { +public class SearchBar : Adw.Bin { /// Translators: Search entry placeholder text private const string DEFAULT_SEARCH_TEXT = _("Search"); + private unowned Gtk.SearchBar search_bar; + public bool search_mode_enabled { + get { return this.search_bar.search_mode_enabled; } + set { this.search_bar.search_mode_enabled = value; } + } + public Gtk.SearchEntry entry { get; private set; default = new Gtk.SearchEntry(); } @@ -23,10 +29,14 @@ public class SearchBar : Hdy.SearchBar { public SearchBar(Geary.Engine engine) { + var bar = new Gtk.SearchBar(); + this.search_bar = bar; + this.child = bar; + this.engine = engine; this.search_undo = new Components.EntryUndo(this.entry); - this.notify["search-mode-enabled"].connect(on_search_mode_changed); + search_bar.notify["search-mode-enabled"].connect(on_search_mode_changed); /// Translators: Search entry tooltip this.entry.tooltip_text = _("Search all mail in account for keywords"); @@ -37,21 +47,18 @@ public class SearchBar : Hdy.SearchBar { search_text_changed(this.entry.text); }); this.entry.placeholder_text = DEFAULT_SEARCH_TEXT; - this.entry.has_focus = true; - var column = new Hdy.Clamp(); + var column = new Adw.Clamp(); column.maximum_size = 400; - column.add(this.entry); + column.child = this.entry; - connect_entry(this.entry); - add(column); - - show_all(); + search_bar.connect_entry(this.entry); + search_bar.child = column; } - public override void grab_focus() { - set_search_mode(true); - this.entry.grab_focus(); + public override bool grab_focus() { + this.search_mode_enabled = true; + return this.entry.grab_focus(); } public void set_account(Geary.Account? account) { diff --git a/src/client/components/components-validator-group.vala b/src/client/components/components-validator-group.vala new file mode 100644 index 00000000..b19f23b9 --- /dev/null +++ b/src/client/components/components-validator-group.vala @@ -0,0 +1,91 @@ +/* + * Copyright 2025 Niels De Graef + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Groups several validators together, allowing to show an aggregate result. + */ +public class Components.ValidatorGroup : GLib.Object, GLib.ListModel, Gtk.Buildable { + + private GenericArray validators = new GenericArray(); + + /** Fired when the relevant validator has changed */ + public signal void changed(Validator validator); + + /** Fired when the relevant validator has emitted the activated signal */ + public signal void activated(Validator validator); + + public void add_validator(Validator validator) { + validator.changed.connect(on_validator_changed); + validator.activated.connect(on_validator_activated); + this.validators.add(validator); + } + + private void on_validator_changed(Validator validator) { + this.changed(validator); + } + + private void on_validator_activated(Validator validator) { + this.activated(validator); + } + + public bool is_valid() { + foreach (unowned var validator in this.validators) { + if (validator.is_valid) + return false; + } + + return true; + } + + // GListModel implementation + + public GLib.Type get_item_type() { + return typeof(Components.Validator); + } + + public uint get_n_items() { + return this.validators.length; + } + + public GLib.Object? get_item(uint index) { + if (index >= this.validators.length) + return null; + return this.validators[index]; + } + + // GtkBuildable implementation + + public void add_child(Gtk.Builder builder, Object child, string? type) { + unowned var validator = child as Validator; + if (validator == null) { + critical("Can't add child %p to ValidatorGroup, expected Validator instance", validator); + return; + } + + add_validator(validator); + } + + private string id; + public void set_id(string id) { + this.id = id; + } + public unowned string get_id() { + return this.id; + } + + // We don't need any of these, but Vala requires us to implement them + public void custom_finished(Gtk.Builder builder, GLib.Object? child, string tagname, void* data) {} + public void custom_tag_end(Gtk.Builder builder, GLib.Object? child, string tagname, void* data) {} + public bool custom_tag_start(Gtk.Builder builder, GLib.Object? child, string tagname, out Gtk.BuildableParser parser, out void* data) { + return false; + } + public unowned GLib.Object get_internal_child(Gtk.Builder builder, string childname) { + return null; + } + public void parser_finished(Gtk.Builder builder) {} + public void set_buildable_property(Gtk.Builder builder, string name, GLib.Value value) {} +} diff --git a/src/client/components/components-validator.vala b/src/client/components/components-validator.vala index 16f256b5..9f3e6634 100644 --- a/src/client/components/components-validator.vala +++ b/src/client/components/components-validator.vala @@ -6,7 +6,7 @@ */ /** - * Validates the contents of a Gtk Entry as they are entered. + * Validates the contents of a Gtk Editable as they are entered. * * This class may be used to validate required, but otherwise free * form entries. Subclasses may perform more complex and task-specific @@ -15,34 +15,30 @@ public class Components.Validator : GLib.Object { - private const Gtk.EntryIconPosition ICON_POS = - Gtk.EntryIconPosition.SECONDARY; - - /** - * The state of the entry monitored by this validator. + * The state of the editable monitored by this validator. * * Only {@link VALID} can be considered strictly valid, all other * states should be treated as being invalid. */ public enum Validity { - /** The contents of the entry have not been validated. */ + /** The contents of the editable have not been validated. */ INDETERMINATE, - /** The contents of the entry is valid. */ + /** The contents of the editable is valid. */ VALID, /** - * The contents of the entry is being checked. + * The contents of the editable is being checked. * * See {@link validate} for the use of this value. */ IN_PROGRESS, - /** The contents of the entry is required but not present. */ + /** The contents of the editable is required but not present. */ EMPTY, - /** The contents of the entry is not valid. */ + /** The contents of the editable is not valid. */ INVALID; } @@ -50,11 +46,11 @@ public class Components.Validator : GLib.Object { public enum Trigger { /** A manual validation was requested via {@link validate}. */ MANUAL, - /** The entry's contents changed. */ + /** The editable's contents changed. */ CHANGED, - /** The entry lost the keyboard focus. */ + /** The editable lost the keyboard focus. */ LOST_FOCUS, - /** The user activated the entry. */ + /** The user activated the editable. */ ACTIVATED; } @@ -64,10 +60,26 @@ public class Components.Validator : GLib.Object { public string? icon_tooltip_text; } - /** The entry being monitored */ - public Gtk.Entry target { get; private set; } + /** The editable being monitored */ + public Gtk.Editable target { + get { return this._target; } + construct set { + this._target = value; + if (value is Gtk.Entry) { + this.target_helper = new EntryHelper((Gtk.Entry) value); + } else if (value is Adw.EntryRow) { + this.target_helper = new EntryRowHelper((Adw.EntryRow) value); + } else { + critical("Validator for type '%s' unsupported", value.get_type().name()); + } + } + } + private Gtk.Editable _target; + protected EditableHelper target_helper; - /** Determines if the current state indicates the entry is valid. */ + private Gtk.EventControllerFocus target_focus_controller = new Gtk.EventControllerFocus(); + + /** Determines if the current state indicates the editable is valid. */ public bool is_valid { get { return (this.state == Validity.VALID); } } @@ -75,40 +87,22 @@ public class Components.Validator : GLib.Object { /** * Determines how empty entries are treated. * - * If true, an empty entry is considered {@link Validity.EMPTY} + * If true, an empty editable is considered {@link Validity.EMPTY} * (i.e. invalid) else it is considered to be {@link * Validity.INDETERMINATE}. */ public bool is_required { get; set; default = true; } - /** The current validation state of the entry. */ + /** The current validation state of the editable. */ public Validity state { get; private set; default = Validity.INDETERMINATE; } - /** The UI state to use when indeterminate. */ - public UiState indeterminate_state; - - /** The UI state to use when valid. */ - public UiState valid_state; - - /** The UI state to use when in progress. */ - public UiState in_progress_state; - - /** The UI state to use when empty. */ - public UiState empty_state; - - /** The UI state to use when invalid. */ - public UiState invalid_state; - // Determines if the value has changed since last validation private bool target_changed = false; private Geary.TimeoutManager ui_update_timer; - private Geary.TimeoutManager pulse_timer; - bool did_pulse = false; - /** Fired when the validation state changes. */ public signal void state_changed(Trigger reason, Validity prev_state); @@ -123,49 +117,28 @@ public class Components.Validator : GLib.Object { public signal void focus_lost(); - public Validator(Gtk.Entry target) { - this.target = target; - + construct { this.ui_update_timer = new Geary.TimeoutManager.seconds( - 2, on_update_ui + 1, on_update_ui ); - this.pulse_timer = new Geary.TimeoutManager.milliseconds( - 200, on_pulse - ); - this.pulse_timer.repetition = FOREVER; - - this.indeterminate_state = { - target.get_icon_name(ICON_POS), - target.get_icon_tooltip_text(ICON_POS) - }; - this.valid_state = { - target.get_icon_name(ICON_POS), - target.get_icon_tooltip_text(ICON_POS) - }; - this.in_progress_state = { - target.get_icon_name(ICON_POS), - null - }; - this.empty_state = { "dialog-warning-symbolic", null }; - this.invalid_state = { "dialog-error-symbolic", null }; - - this.target.add_events(Gdk.EventMask.FOCUS_CHANGE_MASK); - this.target.activate.connect(on_activate); + this.target_helper.activated.connect(on_activate); this.target.changed.connect(on_changed); - this.target.focus_out_event.connect(on_focus_out); + this.target_focus_controller.leave.connect(on_focus_out); + this.target.add_controller(this.target_focus_controller); + } + + + public Validator(Gtk.Editable target) { + GLib.Object(target: target); } ~Validator() { - this.target.focus_out_event.disconnect(on_focus_out); - this.target.changed.disconnect(on_changed); - this.target.activate.disconnect(on_activate); this.ui_update_timer.reset(); - this.pulse_timer.reset(); } /** - * Triggers a validation of the entry. + * Triggers a validation of the editable. * * In the case of an asynchronous validation implementations, * result of the validation will be known sometime after this call @@ -176,12 +149,12 @@ public class Components.Validator : GLib.Object { } /** - * Called to validate the target entry's value. + * Called to validate the target editable's value. * * This method will be called repeatedly as the user edits the - * value of the target entry to set the new validation {@link + * value of the target editable to set the new validation {@link * state} given the updated value. It will *not* be called if the - * entry is changed to be empty, instead the validity state will + * editable is changed to be empty, instead the validity state will * be set based on {@link is_required}. * * Subclasses may override this method to implement custom @@ -195,7 +168,7 @@ public class Components.Validator : GLib.Object { * with the actual result. * * The given reason specifies which user action was taken to cause - * the entry's value to be validated. + * the editable's value to be validated. * * By default, this always returns {@link Validity.VALID}, making * it useful for required, but otherwise free-form fields only. @@ -205,7 +178,7 @@ public class Components.Validator : GLib.Object { } /** - * Updates the current validation state and the entry's UI. + * Updates the current validation state and the editable's UI. * * This should only be called by subclasses that implement a * CPU-intensive or long-running validation routine and it has @@ -263,8 +236,6 @@ public class Components.Validator : GLib.Object { // no-op break; } - } else if (!this.pulse_timer.is_running) { - this.pulse_timer.start(); } } @@ -282,66 +253,12 @@ public class Components.Validator : GLib.Object { private void update_ui(Validity state) { this.ui_update_timer.reset(); - Gtk.StyleContext style = this.target.get_style_context(); - style.remove_class(Gtk.STYLE_CLASS_ERROR); - style.remove_class(Gtk.STYLE_CLASS_WARNING); - - UiState ui = { null, null }; - bool in_progress = false; - switch (state) { - case Validity.INDETERMINATE: - ui = this.indeterminate_state; - break; - - case Validity.VALID: - ui = this.valid_state; - break; - - case Validity.IN_PROGRESS: - in_progress = true; - ui = this.in_progress_state; - break; - - case Validity.EMPTY: - style.add_class(Gtk.STYLE_CLASS_WARNING); - ui = this.empty_state; - break; - - case Validity.INVALID: - style.add_class(Gtk.STYLE_CLASS_ERROR); - ui = this.invalid_state; - break; - } - - if (in_progress) { - if (!this.pulse_timer.is_running) { - this.pulse_timer.start(); - } - } else { - this.pulse_timer.reset(); - // If a pulse hasn't been performed (and hence the - // progress bar is not visible), setting the fraction here - // to reset it will actually cause the progress bar to - // become visible. So only reset if needed. - if (this.did_pulse) { - this.target.progress_fraction = 0.0; - this.did_pulse = false; - } - } - - this.target.set_icon_from_icon_name(ICON_POS, ui.icon_name); - this.target.set_icon_tooltip_text( - ICON_POS, - // Setting the tooltip to null or the empty string can - // cause GTK+ to setfult. See GTK+ issue #1160. - Geary.String.is_empty(ui.icon_tooltip_text) - ? " " : ui.icon_tooltip_text - ); + this.target_helper.update_ui(state); } private void on_activate() { if (this.target_changed) { - validate_entry(Trigger.ACTIVATED); + validate_entry(Trigger.ACTIVATED); } else { activated(); } @@ -351,11 +268,6 @@ public class Components.Validator : GLib.Object { update_ui(this.state); } - private void on_pulse() { - this.target.progress_pulse(); - this.did_pulse = true; - } - private void on_changed() { this.target_changed = true; validate_entry(Trigger.CHANGED); @@ -364,40 +276,161 @@ public class Components.Validator : GLib.Object { this.ui_update_timer.start(); } - private bool on_focus_out() { + private void on_focus_out(Gtk.EventControllerFocus controller) { if (this.target_changed) { // Only update if the widget has lost focus due to not being // the focused widget any more, rather than the whole window // having lost focus. - if (!this.target.is_focus) { + if (!this.target.is_focus()) { validate_entry(Trigger.LOST_FOCUS); } } else { focus_lost(); } - return Gdk.EVENT_PROPAGATE; } } +/** + * A helper class to set an icon, abstracting away the underlying API of the + * Gtk.Editable implementation. + */ +protected abstract class Components.EditableHelper : Object { + + /** Tooltip text in case the validator returns an invalid state */ + public string? invalid_tooltip_text { get; set; default = null; } + + /** Tooltip text in case the validator returns an empty state */ + public string? empty_tooltip_text { get; set; default = null; } + + /** Emitted if the editable has been activated */ + public signal void activated(); + + /** Sets an error icon with the given name and tooltip */ + public abstract void update_ui(Validator.Validity state); +} + + +private class Components.EntryHelper : EditableHelper { + + private unowned Gtk.Entry entry; + + public EntryHelper(Gtk.Entry entry) { + this.entry = entry; + this.entry.activate.connect((e) => this.activated()); + } + + public override void update_ui(Validator.Validity state) { + this.entry.remove_css_class("error"); + this.entry.remove_css_class("warning"); + + switch (state) { + case Validator.Validity.INDETERMINATE: + case Validator.Validity.VALID: + // Reset + this.entry.secondary_icon_name = ""; + this.entry.secondary_icon_tooltip_text = ""; + break; + + case Validator.Validity.IN_PROGRESS: + this.entry.secondary_icon_paintable = new Adw.SpinnerPaintable(this.entry); + this.entry.secondary_icon_tooltip_text = _("Validating"); + break; + + case Validator.Validity.EMPTY: + this.entry.add_css_class("warning"); + this.entry.secondary_icon_name = "dialog-warning-symbolic"; + this.entry.secondary_icon_tooltip_text = this.empty_tooltip_text ?? ""; + break; + + case Validator.Validity.INVALID: + this.entry.add_css_class("error"); + this.entry.secondary_icon_name = "dialog-error-symbolic"; + this.entry.secondary_icon_tooltip_text = this.invalid_tooltip_text ?? ""; + break; + } + } +} + +private class Components.EntryRowHelper : EditableHelper { + + private unowned Adw.EntryRow row; + + private unowned Adw.Spinner? spinner = null; + private unowned Gtk.Image? error_image = null; + + public EntryRowHelper(Adw.EntryRow row) { + this.row = row; + this.row.entry_activated.connect((e) => this.activated()); + } + + public override void update_ui(Validator.Validity state) { + reset(); + + this.row.remove_css_class("error"); + this.row.remove_css_class("warning"); + + switch (state) { + case Validator.Validity.INDETERMINATE: + case Validator.Validity.VALID: + break; + + case Validator.Validity.IN_PROGRESS: + var spinner = new Adw.Spinner(); + spinner.tooltip_text = _("Validating"); + this.row.add_suffix(spinner); + this.spinner = spinner; + break; + + case Validator.Validity.EMPTY: + this.row.add_css_class("warning"); + var img = new Gtk.Image.from_icon_name("dialog-warning-symbolic"); + img.tooltip_text = this.empty_tooltip_text ?? ""; + this.row.add_suffix(img); + this.error_image = img; + break; + + case Validator.Validity.INVALID: + this.row.add_css_class("error"); + var img = new Gtk.Image.from_icon_name("dialog-error-symbolic"); + img.tooltip_text = this.invalid_tooltip_text ?? ""; + this.row.add_suffix(img); + this.error_image = img; + break; + } + } + + private void reset() { + if (this.spinner != null) { + this.row.remove(this.spinner); + this.spinner = null; + } + if (this.error_image != null) { + this.row.remove(this.error_image); + this.error_image = null; + } + } +} + /** - * A validator for GTK Entry widgets that contain an email address. + * A validator for GTK Editable widgets that contain an email address. */ public class Components.EmailValidator : Validator { - public EmailValidator(Gtk.Entry target) { - base(target); - - // Translators: Tooltip used when an entry requires a valid + construct { + // Translators: Tooltip used when an editable requires a valid // email address to be entered, but one is not provided. - this.empty_state.icon_tooltip_text = _("An email address is required"); + this.target_helper.empty_tooltip_text = _("An email address is required"); - // Translators: Tooltip used when an entry requires a valid + // Translators: Tooltip used when an editablerequires a valid // email address to be entered, but the address is invalid. - this.invalid_state.icon_tooltip_text = _("Not a valid email address"); + this.target_helper.invalid_tooltip_text = _("Not a valid email address"); } + public EmailValidator(Gtk.Editable target) { + GLib.Object(target: target); + } protected override Validator.Validity do_validate(string value, Validator.Trigger reason) { @@ -409,9 +442,9 @@ public class Components.EmailValidator : Validator { /** - * A validator for GTK Entry widgets that contain a network address. + * A validator for Gtk.Editable widgets that contain a network address. * - * This attempts parse the entry value as a host name or IP address + * This attempts parse the editable value as a host name or IP address * with an optional port, then resolve the host name if * needed. Parsing is performed by {@link GLib.NetworkAddress.parse} * to parse the user input, hence it may be specified in any form @@ -426,27 +459,30 @@ public class Components.NetworkAddressValidator : Validator { } /** The default port used when parsing the address. */ - public uint16 default_port { get; private set; } + public uint16 default_port { get; construct set; } private GLib.Resolver resolver; private GLib.Cancellable? cancellable = null; - - public NetworkAddressValidator(Gtk.Entry target, uint16 default_port = 0) { - base(target); - this.default_port = default_port; - + construct { this.resolver = GLib.Resolver.get_default(); - // Translators: Tooltip used when an entry requires a valid, + // Translators: Tooltip used when an editable requires a valid, // resolvable server name to be entered, but one is not // provided. - this.empty_state.icon_tooltip_text = _("A server name is required"); + this.target_helper.empty_tooltip_text = _("A server name is required"); - // Translators: Tooltip used when an entry requires a valid + // Translators: Tooltip used when an editable requires a valid // server name to be entered, but it was unable to be // looked-up in the DNS. - this.invalid_state.icon_tooltip_text = _("Could not look up server name"); + this.target_helper.invalid_tooltip_text = _("Could not look up server name"); + } + + public NetworkAddressValidator(Gtk.Editable target, uint16 default_port = 0) { + GLib.Object( + target: target, + default_port: default_port + ); } diff --git a/src/client/components/components-web-view.vala b/src/client/components/components-web-view.vala index 4b0bee34..f093ad9c 100644 --- a/src/client/components/components-web-view.vala +++ b/src/client/components/components-web-view.vala @@ -48,23 +48,6 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { private const string USER_CSS_LEGACY = "user-message.css"; - - // Workaround WK binding ctor not accepting any args - private class WebsiteDataManager : WebKit.WebsiteDataManager { - - public WebsiteDataManager(string base_cache_directory) { - // Use the cache dir for both cache and data since a) - // emails shouldn't be storing data anyway, and b) so WK - // doesn't use the default, shared data dir. - Object( - base_cache_directory: base_cache_directory, - base_data_directory: base_cache_directory - ); - } - - } - - private static WebKit.WebContext? default_context = null; private static GenericArray styles = null; @@ -76,16 +59,12 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { * Initialises WebKit.WebContext for use by the client. */ public static void init_web_context(Application.Configuration config, - File web_extension_dir, - File cache_dir, - bool sandboxed=true) { - WebsiteDataManager data_manager = new WebsiteDataManager(cache_dir.get_path()); - WebKit.WebContext context = new WebKit.WebContext.with_website_data_manager(data_manager); - // Enable WebProcess sandboxing - if (sandboxed) { - context.add_path_to_sandbox(web_extension_dir.get_path(), true); - context.set_sandbox_enabled(true); - } + File web_extension_dir) { + WebKit.WebContext context = new WebKit.WebContext(); + + // Configure WebProcess sandboxing + context.add_path_to_sandbox(web_extension_dir.get_path(), true); + // Use the doc browser model so that we get some caching of // resources between email body loads. context.set_cache_model(WebKit.CacheModel.DOCUMENT_BROWSER); @@ -102,11 +81,11 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { view.handle_internal_request(req); } }); - context.initialize_web_extensions.connect((context) => { - context.set_web_extensions_directory( + context.initialize_web_process_extensions.connect((context) => { + context.set_web_process_extensions_directory( web_extension_dir.get_path() ); - context.set_web_extensions_initialization_user_data( + context.set_web_process_extensions_initialization_user_data( new Variant.boolean(config.enable_debug) ); }); @@ -196,6 +175,7 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { } private static inline uint to_wk2_font_size(Pango.FontDescription font) { + // XXX GTK4 I have no idea what to do here double size = font.get_size(); if (!font.get_size_is_absolute()) { size = size / Pango.SCALE; @@ -331,6 +311,7 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { protected WebView(Application.Configuration config, + GLib.File? cache_dir = null, WebKit.UserContentManager? custom_manager = null, WebView? related = null) { WebKit.Settings setts = new WebKit.Settings(); @@ -345,9 +326,6 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { setts.enable_media_stream = false; setts.enable_offline_web_application_cache = false; setts.enable_page_cache = false; -#if WEBKIT_PLUGINS_SUPPORTED - setts.enable_plugins = false; -#endif setts.hardware_acceleration_policy = WebKit.HardwareAccelerationPolicy.NEVER; setts.javascript_can_access_clipboard = true; @@ -376,10 +354,23 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { WebView.styles.foreach(style => content_manager.add_style_sheet(style)); } + // Use cache dir for both cache & data since a) emails shouldn't be + // storing data anyway, and b) so WK doesn't use the default, shared datadir + WebKit.NetworkSession nw_session; + if (cache_dir != null) { + nw_session = new WebKit.NetworkSession( + cache_dir.get_path(), + cache_dir.get_path() + ); + } else { + nw_session = new WebKit.NetworkSession.ephemeral(); + } + Object( settings: setts, user_content_manager: content_manager, - web_context: WebView.default_context + web_context: WebView.default_context, + network_session: nw_session ); base_ref(); init(config); @@ -408,9 +399,9 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { base_unref(); } - public override void destroy() { + public override void dispose() { this.message_handlers.clear(); - base.destroy(); + base.dispose(); } /** @@ -668,7 +659,9 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { } else if (this.zoom_level > ZOOM_MAX) { this.zoom_level = ZOOM_MAX; } - this.scroll_event.connect(on_scroll_event); + var scroll_controller = new Gtk.EventControllerScroll(Gtk.EventControllerScrollFlags.VERTICAL); + scroll_controller.scroll.connect(on_scroll_event); + add_controller(scroll_controller); // Watch desktop font settings Settings system_settings = config.gnome_interface; @@ -797,15 +790,18 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface { return Gdk.EVENT_STOP; } - private bool on_scroll_event(Gdk.EventScroll event) { - if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) { + private bool on_scroll_event(Gtk.EventControllerScroll scroll_controller, double dx, double dy) { + var event = (Gdk.ScrollEvent) scroll_controller.get_current_event(); + if (Gdk.ModifierType.CONTROL_MASK in event.get_modifier_state()) { double dir = 0; - if (event.direction == Gdk.ScrollDirection.UP) + if (event.get_direction() == Gdk.ScrollDirection.UP) dir = -1; - else if (event.direction == Gdk.ScrollDirection.DOWN) + else if (event.get_direction() == Gdk.ScrollDirection.DOWN) dir = 1; - else if (event.direction == Gdk.ScrollDirection.SMOOTH) - dir = event.delta_y; + else if (event.get_direction() == Gdk.ScrollDirection.SMOOTH) { + double delta_x; + event.get_deltas(out delta_x, out dir); + } if (dir < 0) { zoom_in(); diff --git a/src/client/components/folder-popover.vala b/src/client/components/folder-popover.vala index abbe75c9..be4b3411 100644 --- a/src/client/components/folder-popover.vala +++ b/src/client/components/folder-popover.vala @@ -62,8 +62,7 @@ public class FolderPopover : Gtk.Popover { } var row = new FolderPopoverRow(context, map); - row.show(); - list_box.add(row); + list_box.append(row); list_box.invalidate_sort(); } @@ -90,7 +89,9 @@ public class FolderPopover : Gtk.Popover { [GtkCallback] private void on_unmap(Gtk.Widget widget) { - list_box.foreach((row) => list_box.remove(row)); + unowned var row = this.list_box.get_row_at_index(0); + while (row != null) + this.list_box.remove(row); } [GtkCallback] diff --git a/src/client/components/icon-factory.vala b/src/client/components/icon-factory.vala deleted file mode 100644 index 267ac3e4..00000000 --- a/src/client/components/icon-factory.vala +++ /dev/null @@ -1,142 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -// Singleton class to hold icons. -public class IconFactory { - public const Gtk.IconSize ICON_TOOLBAR = Gtk.IconSize.LARGE_TOOLBAR; - public const Gtk.IconSize ICON_SIDEBAR = Gtk.IconSize.MENU; - - - public static IconFactory? instance { get; private set; } - - - public static void init(GLib.File resource_directory) { - IconFactory.instance = new IconFactory(resource_directory); - } - - - public const int UNREAD_ICON_SIZE = 16; - public const int STAR_ICON_SIZE = 16; - - private Gtk.IconTheme icon_theme { get; private set; } - - private File icons_dir; - - // Creates the icon factory. - private IconFactory(GLib.File resource_directory) { - icons_dir = resource_directory.get_child("icons"); - icon_theme = Gtk.IconTheme.get_default(); - icon_theme.append_search_path(icons_dir.get_path()); - } - - private int icon_size_to_pixels(Gtk.IconSize icon_size) { - switch (icon_size) { - case ICON_SIDEBAR: - return 16; - - case ICON_TOOLBAR: - default: - return 24; - } - } - - public Icon get_theme_icon(string name) { - return new ThemedIcon(name); - } - - public Icon get_custom_icon(string name, Gtk.IconSize size) { - int pixels = icon_size_to_pixels(size); - - // Try sized icon first. - File icon_file = icons_dir.get_child("%dx%d".printf(pixels, pixels)).get_child( - "%s.svg".printf(name)); - - // If that wasn't found, try a non-sized icon. - if (!icon_file.query_exists()) - icon_file = icons_dir.get_child("%s.svg".printf(name)); - - return new FileIcon(icon_file); - } - - // Attempts to load and return the missing image icon. - private Gdk.Pixbuf? get_missing_icon(int size, Gtk.IconLookupFlags flags = 0) { - try { - return icon_theme.load_icon("image-missing", size, flags); - } catch (Error err) { - warning("Couldn't load image-missing icon: %s", err.message); - } - - // If that fails... well they're out of luck. - return null; - } - - public Gtk.IconInfo? lookup_icon(string icon_name, int size, Gtk.IconLookupFlags flags = 0) { - Gtk.IconInfo? icon_info = icon_theme.lookup_icon(icon_name, size, flags); - if (icon_info == null) { - icon_info = icon_theme.lookup_icon("text-x-generic-symbolic", size, flags); - } - return icon_info; - } - - // GTK+ 3.14 no longer scales icons via the IconInfo, so perform manually until we - // properly install the icons as per 3.14's expectations. - private Gdk.Pixbuf aspect_scale_down_pixbuf(Gdk.Pixbuf pixbuf, int size) { - if (pixbuf.width <= size && pixbuf.height <= size) - return pixbuf; - - int scaled_width, scaled_height; - if (pixbuf.width >= pixbuf.height) { - double aspect = (double) size / (double) pixbuf.width; - scaled_width = size; - scaled_height = (int) Math.round((double) pixbuf.height * aspect); - } else { - double aspect = (double) size / (double) pixbuf.height; - scaled_width = (int) Math.round((double) pixbuf.width * aspect); - scaled_height = size; - } - - return pixbuf.scale_simple(scaled_width, scaled_height, Gdk.InterpType.BILINEAR); - } - - public Gdk.Pixbuf? load_symbolic(string icon_name, int size, Gtk.StyleContext style, - Gtk.IconLookupFlags flags = 0) { - Gtk.IconInfo? icon_info = icon_theme.lookup_icon(icon_name, size, flags); - - // Attempt to load as a symbolic icon. - if (icon_info != null) { - try { - return aspect_scale_down_pixbuf(icon_info.load_symbolic_for_context(style), size); - } catch (Error e) { - message("Couldn't load icon: %s", e.message); - } - } - - // Default: missing image icon. - return get_missing_icon(size, flags); - } - - /** - * Loads a symbolic icon into a pixbuf, where the color-key has been switched to the provided - * color. - */ - public Gdk.Pixbuf? load_symbolic_colored(string icon_name, int size, Gdk.RGBA color, - Gtk.IconLookupFlags flags = 0) { - Gtk.IconInfo? icon_info = icon_theme.lookup_icon(icon_name, size, flags); - - // Attempt to load as a symbolic icon. - if (icon_info != null) { - try { - return aspect_scale_down_pixbuf(icon_info.load_symbolic(color), size); - } catch (Error e) { - warning("Couldn't load icon: %s", e.message); - } - } - // Default: missing image icon. - return get_missing_icon(size, flags); - } - -} - diff --git a/src/client/components/monitored-progress-bar.vala b/src/client/components/monitored-progress-bar.vala index 52afa599..925a6ad8 100644 --- a/src/client/components/monitored-progress-bar.vala +++ b/src/client/components/monitored-progress-bar.vala @@ -7,28 +7,35 @@ /** * Adapts a progress bar to automatically display progress of a Geary.ProgressMonitor. */ -public class MonitoredProgressBar : Gtk.ProgressBar { +public class MonitoredProgressBar : Adw.Bin { private Geary.ProgressMonitor? monitor = null; + private Gtk.ProgressBar progress_bar; + + construct { + this.progress_bar = new Gtk.ProgressBar(); + this.child = this.progress_bar; + } + public void set_progress_monitor(Geary.ProgressMonitor monitor) { this.monitor = monitor; monitor.start.connect(on_start); monitor.finish.connect(on_finish); monitor.update.connect(on_update); - fraction = monitor.progress; + this.progress_bar.fraction = monitor.progress; } private void on_start() { - fraction = 0.0; + this.progress_bar.fraction = 0.0; } private void on_update(double total_progress, double change, Geary.ProgressMonitor monitor) { - fraction = total_progress; + this.progress_bar.fraction = total_progress; } private void on_finish() { - fraction = 1.0; + this.progress_bar.fraction = 1.0; } } diff --git a/src/client/components/monitored-spinner.vala b/src/client/components/monitored-spinner.vala index 1031c08e..88f87044 100644 --- a/src/client/components/monitored-spinner.vala +++ b/src/client/components/monitored-spinner.vala @@ -7,9 +7,16 @@ /** * Adapts a progress spinner to automatically display progress of a Geary.ProgressMonitor. */ -public class MonitoredSpinner : Gtk.Spinner { +public class MonitoredSpinner : Adw.Bin { private Geary.ProgressMonitor? monitor = null; + private Adw.Spinner spinner; + + construct { + this.spinner = new Adw.Spinner(); + this.child = spinner; + } + public void set_progress_monitor(Geary.ProgressMonitor? monitor) { if (monitor != null) { this.monitor = monitor; @@ -17,8 +24,7 @@ public class MonitoredSpinner : Gtk.Spinner { monitor.finish.connect(on_stop); } else { this.monitor = null; - stop(); - hide(); + this.spinner.visible = false; } } @@ -28,13 +34,11 @@ public class MonitoredSpinner : Gtk.Spinner { } private void on_start() { - start(); - show(); + this.spinner.visible = true; } private void on_stop() { - stop(); - hide(); + this.spinner.visible = false; } } diff --git a/src/client/composer/composer-addresses-row.vala b/src/client/composer/composer-addresses-row.vala new file mode 100644 index 00000000..8ea1ff78 --- /dev/null +++ b/src/client/composer/composer-addresses-row.vala @@ -0,0 +1,457 @@ +/* + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2025 Niels De Graef + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A widget that allows a user to create a list of email addresses + * (for example a "CC" row). + * + *XXX we need to put an EntryUndo back here + */ +public class Composer.AddressesRow : Adw.EntryRow, Geary.BaseInterface { + + /** + * The list of email addresses. + * + * Check the "is-valid" property to see if they are actually valid. + * + * Manually setting this property will override any text that was there before. + */ + public Geary.RFC822.MailboxAddresses addresses { + get { return this._addresses; } + set { + this._addresses = value; + validate_addresses(); + this.text = value.to_full_display(); + } + } + private Geary.RFC822.MailboxAddresses _addresses = new Geary.RFC822.MailboxAddresses(); + + /** Determines if the entry contains only valid email addresses (and is not empty) */ + public bool is_valid { get; private set; default = false; } + + public bool is_empty { + //XXX could this be this.text.length != 0 insteaD? + get { return this.addresses.is_empty; } + } + + // Text between the start of the entry or end of the previous email + // address and the current position of the cursor, if any. + // This will be used for searching a match in the contact list. + //XXX maybe replace this with a filter? + public string search_key { + get { return this._search_key; } + set { + if (this._search_key == value) + return; + string old_value = this._search_key; + this._search_key = value; + update_search_filter(old_value, value); + notify_property("search-key"); + } + } + private string _search_key = ""; + + // The list of (possibly incomplete) email addresses + private GenericArray addresses_raw = new GenericArray(); + // Index in addressew_raw of the email address the cursor is currently at + private int cursor_at_address = 0; + + public Application.ContactStore? contacts { get; set; default = null; } + + private unowned AddressSuggestionPopover popover; + + static construct { + set_css_name("geary-composer-widget-header-row"); + } + + construct { + this.changed.connect(on_changed); + + var popover = new AddressSuggestionPopover(); + bind_property("contacts", popover, "contacts", BindingFlags.SYNC_CREATE); + popover.selected_address.connect(on_address_suggestion_selected); + popover.set_parent(this); + this.popover = popover; + + // We can't use the default autohide behavior since it grabs focus, + // which we don't want (as the user should be able to continue typing). + popover.autohide = false; + } + + public AddressesRow(string title) { + Object(title: title); + base_ref(); + } + + ~AddressesRow() { + base_unref(); + } + + private void validate_addresses() { + bool is_valid = !this._addresses.is_empty; + foreach (Geary.RFC822.MailboxAddress address in this.addresses) { + if (!address.is_valid()) { + is_valid = false; + return; + } + } + this.is_valid = is_valid; + } + + private void on_changed() { + update_addresses(); + update_validity(); + } + + private void update_addresses() { + string current_key = ""; + this.cursor_at_address = 0; + this.addresses_raw.length = 0; + + // NB: Do not strip any white space from the addresses, + // otherwise we won't be able to accurately insert + // addresses in the middle of the list in + // ::insert_address_at_cursor. + + int current_char = 0; + unichar c = 0; + int start_idx = 0; + int next_idx = 0; + bool in_quote = false; + while (this.text.get_next_char(ref next_idx, out c)) { + if (current_char == (this.text.char_count() - 1) && + current_char != 0) { + if (c != ',') { + // Strip whitespace here though so it does not + // interfere with search and highlighting. + current_key = this.text.slice( + start_idx, next_idx + ).strip(); + } + // We're in the middle of the address, so it + // hasn't yet been added to the list and hence we + // don't need to subtract 1 from its size here + this.cursor_at_address = this.addresses_raw.length; + } + + switch (c) { + case ',': + if (!in_quote) { + // Don't include the comma in the address + string address = this.text.slice(start_idx, next_idx - 1); + this.addresses_raw.add(address); + // Don't include it in the next one, either + start_idx = next_idx; + } + break; + + case '"': + in_quote = !in_quote; + break; + } + + current_char++; + } + + // Add any remaining text after the last comma + string address = this.text.substring(start_idx); + this.addresses_raw.add(address); + + // XXX we probably want to do this with a timeout + // Update current key + this.search_key = current_key; + } + + private void update_validity() { + if (Geary.String.is_empty_or_whitespace(text)) { + this._addresses = new Geary.RFC822.MailboxAddresses(); + this.is_valid = false; + } else { + try { + this._addresses = + new Geary.RFC822.MailboxAddresses.from_rfc822_string(text); + this.is_valid = true; + } catch (Geary.RFC822.Error err) { + this._addresses = new Geary.RFC822.MailboxAddresses(); + this.is_valid = false; + } + } + + //XXX we should make this conditional + notify_property("addresses"); + notify_property("is-valid"); + notify_property("is-empty"); + } + + private void update_search_filter(string old_value, string new_value) { + if (this.contacts == null) + return; + + if (new_value.length > 3) { + this.popover.search_contacts.begin(new_value, null, (obj, res) => { + this.popover.search_contacts.end(res); + }); + } else { + this.popover.clear_suggestions(); + } + } + + private void on_address_suggestion_selected(AddressSuggestionPopover popover, + Geary.RFC822.MailboxAddress address) { + insert_address_at_cursor(address); + } + + private void insert_address_at_cursor(Geary.RFC822.MailboxAddress mailbox) { + // Take care to do a delete then an insert here so that + // Component.EntryUndo can combine the two into a single + // undoable command. + + int start_char = 0; + if (this.cursor_at_address > 0) { + // Address parts don't contain commas, so need to add + // an char width for it. Don't need to worry about + // spaces because they are preserved by + // ::update_addresses. + start_char++; + for (uint i = 0; i < this.cursor_at_address; i++) { + start_char += this.addresses_raw[i].char_count(); + } + } + int end_char = get_position(); + + // Format and use the selected address + string formatted = mailbox.to_full_display(); + if (this.cursor_at_address != 0) { + // Isn't the first address, so add some whitespace to + // pad it out + formatted = " " + formatted; + } + if (get_position() < this.text.char_count() && + this.addresses_raw[this.cursor_at_address].strip() != + this.search_key.strip()) { + // Isn't at the end of the entry, and the address + // under the cursor does not simply consist of the + // lookup key (i.e. is effectively already empty + // otherwise), so add a comma to separate this address + // from the next one + formatted = formatted + ", "; + } + this.addresses_raw.insert(this.cursor_at_address, formatted); + + // Update the entry text + if (start_char < end_char) { + delete_text(start_char, end_char); + } + insert_text(formatted, -1, ref start_char); + + // Update the entry cursor position. The previous call + // updates the start so just use that, but add extra space + // for the comma and any white space at the start of the + // next address. + if (start_char < this.text.char_count()) { + start_char += 2; + } + set_position(start_char); + } +} + +/** + * A helper object to list not just the addresses but also their related contact + */ +public class ContactAddressItem : GLib.Object { + public Application.Contact contact { get; construct set; } + public Geary.RFC822.MailboxAddress address { get; construct set; } + + public ContactAddressItem(Application.Contact contact, + Geary.RFC822.MailboxAddress address) { + Object(contact: contact, address: address); + } +} + +public class AddressSuggestionPopover : Gtk.Popover { + + // Minimum visibility for the contact to appear in autocompletion. + private const Geary.Contact.Importance VISIBILITY_THRESHOLD = + Geary.Contact.Importance.RECEIVED_FROM; + + public Application.ContactStore contacts { get; construct set; } + + private GLib.ListStore model; + private Gtk.SingleSelection selection; + + /** + * Fired when the user has selected an address suggestion + */ + public signal void selected_address(Geary.RFC822.MailboxAddress address); + + construct { + var factory = new Gtk.SignalListItemFactory(); + factory.setup.connect(on_setup_item); + factory.bind.connect(on_bind_item); + + this.model = new GLib.ListStore(typeof(ContactAddressItem)); + this.model.items_changed.connect((model, pos, removed, added) => { + bool is_empty = (model.get_n_items() == 0); + bool was_empty = ((model.get_n_items() - added + removed) == 0); + if (is_empty != was_empty) { + if (was_empty) + popup(); + else + popdown(); + } + }); + this.selection = new Gtk.SingleSelection(this.model); + + var listview = new Gtk.ListView(selection, factory); + listview.single_click_activate = true; + listview.tab_behavior = Gtk.ListTabBehavior.ITEM; + listview.activate.connect(on_activate); + + var sw = new Gtk.ScrolledWindow(); + sw.hscrollbar_policy = Gtk.PolicyType.NEVER; + sw.propagate_natural_height = true; + sw.max_content_height = 300; + sw.child = listview; + this.child = sw; + } + + private void on_setup_item(Object object) { + unowned var item = (Gtk.ListItem) object; + + // Create the row widget + var row = new AddressSuggestionRow(); + item.child = row; + } + + private void on_bind_item(Object object) { + unowned var item = (Gtk.ListItem) object; + unowned var row = (AddressSuggestionRow) item.child; + unowned var contact_address = (ContactAddressItem) item.item; + + row.contact_address = contact_address; + } + + private void on_activate(Gtk.ListView listvieww, + uint position) { + var contact_addr = (ContactAddressItem?) this.selection.selected_item; + if (contact_addr == null) + return; + + popdown(); + selected_address(contact_addr.address); + } + + public void clear_suggestions() { + model.remove_all(); + } + + public async void search_contacts(string query, + GLib.Cancellable? cancellable) { + Gee.Collection? results = null; + try { + results = yield this.contacts.search( + query, + VISIBILITY_THRESHOLD, + 20, + cancellable + ); + } catch (GLib.IOError.CANCELLED err) { + // All good + } catch (GLib.Error err) { + debug("Error searching contacts for completion: %s", err.message); + } + + if (!cancellable.is_cancelled()) { + model.remove_all(); + foreach (Application.Contact contact in results) { + for (uint i = 0; i < contact.email_addresses.get_n_items(); i++) { + var addr = (Geary.RFC822.MailboxAddress) contact.email_addresses.get_item(i); + model.append(new ContactAddressItem(contact, addr)); + } + } + } + } +} + +private class AddressSuggestionRow : Gtk.Box { + + private unowned Adw.Avatar avatar; + private unowned Gtk.Label name_label; + private unowned Gtk.Label address_label; + + public ContactAddressItem? contact_address { + get { return this._contact_address; } + set { + if (this._contact_address == value) + return; + + update(value); + notify_property("contact-address"); + } + } + private ContactAddressItem? _contact_address = null; + + construct { + this.orientation = Gtk.Orientation.HORIZONTAL; + this.spacing = 6; + this.margin_top = 3; + this.margin_bottom = 3; + this.margin_start = 3; + this.margin_end = 3; + + add_css_class("contact-address-list-row"); + + var avatar = new Adw.Avatar(32, null, true); + append(avatar); + this.avatar = avatar; + + var names_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 3); + append(names_vbox); + + var label = new Gtk.Label(""); + label.ellipsize = Pango.EllipsizeMode.END; + label.valign = Gtk.Align.CENTER; + label.halign = Gtk.Align.START; + label.xalign = 0; + label.width_chars = 24; + names_vbox.append(label); + this.name_label = label; + + label = new Gtk.Label(""); + label.ellipsize = Pango.EllipsizeMode.END; + label.valign = Gtk.Align.CENTER; + label.halign = Gtk.Align.START; + label.xalign = 0; + label.width_chars = 24; + names_vbox.append(label); + this.address_label = label; + } + + private void update(ContactAddressItem contact_addr) { + this._contact_address = contact_addr; + + if (contact_addr == null) { + this.avatar.text = null; + this.name_label.label = ""; + this.address_label.label = ""; + return; + } + + //XXX GTK4 + if (Geary.String.is_empty(contact_addr.contact.display_name)) { + this.avatar.text = null; + this.name_label.label = ""; + this.name_label.visible = false; + } else { + this.avatar.text = contact_addr.contact.display_name; + this.name_label.label = contact_addr.contact.display_name; + this.name_label.visible = true; + } + this.address_label.label = contact_addr.address.address; + } +} diff --git a/src/client/composer/composer-application-interface.vala b/src/client/composer/composer-application-interface.vala index 542fbbed..435172f8 100644 --- a/src/client/composer/composer-application-interface.vala +++ b/src/client/composer/composer-application-interface.vala @@ -25,4 +25,5 @@ internal interface Composer.ApplicationInterface : internal abstract async void discard_composed_email(Composer.Widget composer); + internal abstract GLib.File get_web_cache_dir(); } diff --git a/src/client/composer/composer-box.vala b/src/client/composer/composer-box.vala index dfe5a5ca..7e6e2aa5 100644 --- a/src/client/composer/composer-box.vala +++ b/src/client/composer/composer-box.vala @@ -12,7 +12,7 @@ * Adding a composer to this container places it in {@link * Widget.PresentationMode.PANED} mode. */ -public class Composer.Box : Gtk.Frame, Container { +public class Composer.Box : Gtk.Frame, Composer.Container { static construct { set_css_name("geary-composer-box"); @@ -21,7 +21,7 @@ public class Composer.Box : Gtk.Frame, Container { /** {@inheritDoc} */ public Gtk.ApplicationWindow? top_window { - get { return get_toplevel() as Gtk.ApplicationWindow; } + get { return get_root() as Gtk.ApplicationWindow; } } /** {@inheritDoc} */ @@ -39,14 +39,14 @@ public class Composer.Box : Gtk.Frame, Container { this.composer.set_mode(PANED); this.headerbar = headerbar; - this.headerbar.set_conversation_header(composer.header); + this.headerbar.set_conversation_header(composer.header.headerbar); - get_style_context().add_class("geary-composer-box"); + add_css_class("geary-composer-box"); this.halign = Gtk.Align.FILL; this.vexpand = true; this.vexpand_set = true; - add(this.composer); + this.child = this.composer; show(); } @@ -54,8 +54,8 @@ public class Composer.Box : Gtk.Frame, Container { public void close() { vanished(); - this.headerbar.remove_conversation_header(composer.header); - remove(this.composer); + this.headerbar.remove_conversation_header(composer.header.headerbar); + this.child = null; destroy(); } diff --git a/src/client/composer/composer-editor.vala b/src/client/composer/composer-editor.vala index c5d3e95d..bf399fab 100644 --- a/src/client/composer/composer-editor.vala +++ b/src/client/composer/composer-editor.vala @@ -6,8 +6,9 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -[CCode (cname = "components_reflow_box_get_type")] -private extern Type components_reflow_box_get_type(); +//XXX GTK4 +// [CCode (cname = "components_reflow_box_get_type")] +// private extern Type components_reflow_box_get_type(); /** * A widget for editing the body of an email message. @@ -33,7 +34,6 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { private const string ACTION_PASTE_WITHOUT_FORMATTING = "paste-without-formatting"; private const string ACTION_REMOVE_FORMAT = "remove-format"; private const string ACTION_SELECT_ALL = "select-all"; - private const string ACTION_SELECT_DICTIONARY = "select-dictionary"; private const string ACTION_SHOW_FORMATTING = "show-formatting"; private const string ACTION_STRIKETHROUGH = "strikethrough"; internal const string ACTION_TEXT_FORMAT = "text-format"; @@ -71,7 +71,6 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { { ACTION_PASTE_WITHOUT_FORMATTING, on_paste_without_formatting }, { ACTION_REMOVE_FORMAT, on_remove_format, null, "false" }, { ACTION_SELECT_ALL, on_select_all }, - { ACTION_SELECT_DICTIONARY, on_select_dictionary }, { ACTION_SHOW_FORMATTING, on_toggle_action, null, "false", on_show_formatting }, { ACTION_STRIKETHROUGH, on_action, null, "false" }, @@ -127,7 +126,7 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { private Menu context_menu_webkit_text_entry; private Menu context_menu_inspector; - [GtkChild] private unowned Gtk.Grid body_container; + [GtkChild] private unowned Adw.Bin body_bin; [GtkChild] private unowned Gtk.Label message_overlay_label; @@ -147,15 +146,16 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { [GtkChild] private unowned Gtk.Image font_color_icon; [GtkChild] private unowned Gtk.MenuButton more_options_button; - private Gtk.GestureMultiPress click_gesture; + private Gtk.GestureClick click_gesture; internal signal void insert_image(bool from_clipboard); - internal Editor(Application.Configuration config) { + internal Editor(Application.Configuration config, GLib.File cache_dir) { base_ref(); - components_reflow_box_get_type(); + //XXX GTK4 + // components_reflow_box_get_type(); this.config = config; Gtk.Builder builder = new Gtk.Builder.from_resource( @@ -168,7 +168,7 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { this.context_menu_webkit_spelling = (Menu) builder.get_object("context_menu_webkit_spelling"); this.context_menu_webkit_text_entry = (Menu) builder.get_object("context_menu_webkit_text_entry"); - this.body = new WebView(config); + this.body = new WebView(config, cache_dir); this.body.command_stack_changed.connect(on_command_state_changed); this.body.context_menu.connect(on_context_menu); this.body.cursor_context_changed.connect(on_cursor_context_changed); @@ -178,12 +178,13 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { this.body.set_hexpand(true); this.body.set_vexpand(true); this.body.show(); - this.body_container.add(this.body); + this.body_bin.child = this.body; - this.click_gesture = new Gtk.GestureMultiPress(this.body); + this.click_gesture = new Gtk.GestureClick(); this.click_gesture.propagation_phase = CAPTURE; this.click_gesture.pressed.connect(this.on_button_press); this.click_gesture.released.connect(this.on_button_release); + this.body.add_controller(this.click_gesture); this.actions.add_action_entries(ACTIONS, this); this.actions.change_action_state( @@ -219,16 +220,15 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { base_unref(); } - public override void destroy() { + public override void dispose() { this.show_background_work_timeout.reset(); this.background_work_pulse.reset(); - base.destroy(); + base.dispose(); } /** Adds an action bar to the composer. */ public void add_action_bar(Gtk.ActionBar to_add) { - this.action_bar_box.pack_start(to_add); - this.action_bar_box.reorder_child(to_add, 0); + this.action_bar_box.prepend(to_add); } /** @@ -304,10 +304,12 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { } private async void update_color_icon(Gdk.RGBA color) { - var theme = Gtk.IconTheme.get_default(); - var icon = theme.lookup_icon("font-color-symbolic", 16, 0); - var fg_color = Util.Gtk.rgba(0, 0, 0, 1); - this.get_style_context().lookup_color("theme_fg_color", out fg_color); + // XXX GTK4 - need to look into this +#if 0 + var theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); + var icon = theme.lookup_icon("font-color-symbolic", null, 16, 1, Gtk.TextDirection.NONE, 0); + Gdk.RGBA fg_color = { 0, 0, 0, 1 }; + get_style_context().lookup_color("theme_fg_color", out fg_color); try { var pixbuf = yield icon.load_symbolic_async( @@ -318,6 +320,7 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { warning("Could not load icon `font-color-symbolic`!"); this.font_color_icon.icon_name = "font-color-symbolic"; } +#endif } private GLib.SimpleAction? get_action(string action_name) { @@ -343,7 +346,7 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { LinkPopover.Type.EXISTING_LINK, this.pointer_url, (obj, res) => { LinkPopover popover = this.new_link_popover.end(res); - popover.set_relative_to(this.body); + popover.set_parent(this.body); popover.set_pointing_to(location); popover.popup(); }); @@ -352,7 +355,6 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { private bool on_context_menu(WebKit.WebView view, WebKit.ContextMenu context_menu, - Gdk.Event event, WebKit.HitTestResult hit_test_result) { // This is a three step process: // 1. Work out what existing menu items exist that we want to keep @@ -535,11 +537,8 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { action.set_state(new_state); update_formatting_toolbar(); - this.update_color_icon.begin(Util.Gtk.rgba(0, 0, 0, 0)); - } - - private void on_select_dictionary(SimpleAction action, Variant? param) { - this.select_dictionary_button.toggled(); + Gdk.RGBA color = { 0, 0, 0, 0 }; + this.update_color_icon.begin(color); } private void on_command_state_changed(bool can_undo, bool can_redo) { @@ -568,18 +567,24 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { } private void on_copy_link(SimpleAction action, Variant? param) { - Gtk.Clipboard c = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); + Gdk.Clipboard c = get_clipboard(); // XXX could this also be the cursor URL? We should be getting // the target URLn as from the action param - c.set_text(this.pointer_url, -1); - c.store(); + c.set_text(this.pointer_url); + c.store_async.begin(Priority.DEFAULT, null, (obj, res) => { + try { + c.store_async.end(res); + } catch (Error err) { + debug("Couldn't store clipboard: %s", err.message); + } + }); } private void on_paste() { if (this.body.is_rich_text) { // Check for pasted image in clipboard - Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); - bool has_image = clipboard.wait_is_image_available(); + Gdk.Clipboard clipboard = get_clipboard(); + bool has_image = clipboard.formats.contain_gtype(typeof(Gdk.Texture)); if (has_image) { insert_image(true); } else { @@ -643,7 +648,7 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { style.set_state(NORMAL); }); - popover.set_relative_to(this.insert_link_button); + popover.set_parent(this.insert_link_button); popover.popup(); style.set_state(ACTIVE); }); @@ -684,19 +689,24 @@ public class Composer.Editor : Gtk.Grid, Geary.BaseInterface { } private void on_select_color() { - var dialog = new Gtk.ColorChooserDialog( - _("Select Color"), - get_toplevel() as Gtk.Window - ); - if (dialog.run() == Gtk.ResponseType.OK) { - var rgba = dialog.get_rgba(); + var dialog = new Gtk.ColorDialog(); + dialog.title = _("Select Color"); + + dialog.choose_rgba.begin(get_root() as Gtk.Window, null, null, (obj, res) => { + Gdk.RGBA? rgba = null; + try { + rgba = dialog.choose_rgba.end(res); + } catch (Error err) { + debug("Couldn't select color: %s", err.message); + } + if (rgba == null) + return; + this.body.execute_editing_command_with_argument( "forecolor", rgba.to_string() ); - this.update_color_icon.begin(rgba); - } - dialog.destroy(); + }); } private void on_action(GLib.SimpleAction action, GLib.Variant? param) { diff --git a/src/client/composer/composer-email-entry.vala b/src/client/composer/composer-email-entry.vala index 6b495510..54b0cf04 100644 --- a/src/client/composer/composer-email-entry.vala +++ b/src/client/composer/composer-email-entry.vala @@ -47,9 +47,10 @@ public class Composer.EmailEntry : Gtk.Entry { public EmailEntry(Composer.Widget composer) { changed.connect(on_changed); - key_press_event.connect(on_key_press); + Gtk.EventControllerKey key_controller = new Gtk.EventControllerKey(); + key_controller.key_pressed.connect(on_key_pressed); + add_controller(key_controller); this.composer = composer; - show(); } /** Marks the entry as being modified. */ @@ -71,11 +72,14 @@ public class Composer.EmailEntry : Gtk.Entry { private void on_changed() { this.is_modified = true; + //XXX GTK4 see completion class +#if 0 ContactEntryCompletion? completion = get_completion() as ContactEntryCompletion; if (completion != null) { completion.update_model(); } +#endif if (Geary.String.is_empty_or_whitespace(text)) { this._addresses = new Geary.RFC822.MailboxAddresses(); @@ -92,9 +96,14 @@ public class Composer.EmailEntry : Gtk.Entry { } } - private bool on_key_press(Gtk.Widget widget, Gdk.EventKey event) { + private bool on_key_pressed(Gtk.EventControllerKey key_controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { bool propagate = Gdk.EVENT_PROPAGATE; - if (event.keyval == Gdk.Key.Tab) { + if (keyval == Gdk.Key.Tab) { + //XXX GTK4 see completion class +#if 0 // If there is a completion entry selected, then use that ContactEntryCompletion? completion = ( get_completion() as ContactEntryCompletion @@ -104,10 +113,11 @@ public class Composer.EmailEntry : Gtk.Entry { composer.child_focus(Gtk.DirectionType.TAB_FORWARD); propagate = Gdk.EVENT_STOP; } +#endif } if (propagate == Gdk.EVENT_PROPAGATE && - event.keyval != Gdk.Key.Escape) { + keyval != Gdk.Key.Escape) { // Keyboard shortcuts for undo/redo won't work when the // completion UI is visible unless we explicitly check for // them there. @@ -115,9 +125,9 @@ public class Composer.EmailEntry : Gtk.Entry { // However, don't forward it on if the button pressed is // Escape, so that the completion is hidden if present // before the composer is closed. - Gtk.Window? window = get_toplevel() as Gtk.Window; + Gtk.Window? window = get_root() as Gtk.Window; if (window != null) { - propagate = window.activate_key(event); + propagate = key_controller.forward(window); } } return propagate; diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala index 56bbf422..354a1af1 100644 --- a/src/client/composer/composer-embed.vala +++ b/src/client/composer/composer-embed.vala @@ -13,7 +13,7 @@ * Widget.PresentationMode.INLINE} or {@link * Widget.PresentationMode.INLINE_COMPACT} mode. */ -public class Composer.Embed : Gtk.EventBox, Container { +public class Composer.Embed : Adw.Bin, Container { private const int MIN_EDITOR_HEIGHT = 200; @@ -25,7 +25,7 @@ public class Composer.Embed : Gtk.EventBox, Container { /** {@inheritDoc} */ public Gtk.ApplicationWindow? top_window { - get { return get_toplevel() as Gtk.ApplicationWindow; } + get { return get_root() as Gtk.ApplicationWindow; } } /** The email this composer was originally a reply to. */ @@ -57,26 +57,27 @@ public class Composer.Embed : Gtk.EventBox, Container { this.outer_scroller = outer_scroller; - get_style_context().add_class("geary-composer-embed"); + add_css_class("geary-composer-embed"); this.halign = Gtk.Align.FILL; this.vexpand = true; this.vexpand_set = true; - add(composer); - realize.connect(on_realize); - show(); + this.child = composer; + //XXX GTK4 see below + // realize.connect(on_realize); } /** {@inheritDoc} */ public void close() { - disable_scroll_reroute(this); + //XXX GTK4 see below + // disable_scroll_reroute(this); vanished(); - this.composer.free_header(); - remove(this.composer); - destroy(); + this.child = null; } + //XXX GTK4 I think scrolling has changed enough that we might need to rewrite this completely +#if 0 private void on_realize() { reroute_scroll_handling(this); } @@ -84,19 +85,19 @@ public class Composer.Embed : Gtk.EventBox, Container { private void reroute_scroll_handling(Gtk.Widget widget) { widget.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK); widget.scroll_event.connect(on_inner_scroll_event); - Gtk.Container? container = widget as Gtk.Container; - if (container != null) { - foreach (Gtk.Widget child in container.get_children()) - reroute_scroll_handling(child); + unowned Gtk.Widget? child = widget.get_first_child(); + while (child != null) { + reroute_scroll_handling(child); + child = child.get_next_sibling(); } } private void disable_scroll_reroute(Gtk.Widget widget) { widget.scroll_event.disconnect(on_inner_scroll_event); - Gtk.Container? container = widget as Gtk.Container; - if (container != null) { - foreach (Gtk.Widget child in container.get_children()) - disable_scroll_reroute(child); + unowned Gtk.Widget? child = widget.get_first_child(); + while (child != null) { + disable_scroll_reroute(child); + child = child.get_next_sibling(); } } @@ -203,5 +204,6 @@ public class Composer.Embed : Gtk.EventBox, Container { } return ret; } +#endif } diff --git a/src/client/composer/composer-headerbar.vala b/src/client/composer/composer-headerbar.vala index 1ff1fd7f..75c3cfa8 100644 --- a/src/client/composer/composer-headerbar.vala +++ b/src/client/composer/composer-headerbar.vala @@ -4,8 +4,9 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +//XXX GTK4 rename headerbar to header in files too [GtkTemplate (ui = "/org/gnome/Geary/composer-headerbar.ui")] -public class Composer.Headerbar : Hdy.HeaderBar { +public class Composer.Header : Adw.Bin { public bool show_save_and_close { @@ -22,6 +23,8 @@ public class Composer.Headerbar : Hdy.HeaderBar { private bool is_attached = true; + public Adw.HeaderBar headerbar; + [GtkChild] private unowned Gtk.Box detach_start; [GtkChild] private unowned Gtk.Box detach_end; [GtkChild] private unowned Gtk.Button recipients_button; @@ -34,23 +37,24 @@ public class Composer.Headerbar : Hdy.HeaderBar { public signal void expand_composer(); - public Headerbar(Application.Configuration config) { + public Header(Application.Configuration config) { this.config = config; + this.headerbar = (Adw.HeaderBar) this.child; Gtk.Settings.get_default().notify["gtk-decoration-layout"].connect( on_gtk_decoration_layout_changed ); } - public override void destroy() { + public override void dispose() { Gtk.Settings.get_default().notify["gtk-decoration-layout"].disconnect( on_gtk_decoration_layout_changed ); - base.destroy(); + base.dispose(); } public void set_recipients(string label, string tooltip) { - recipients_label.label = label; - recipients_button.tooltip_text = tooltip; + this.recipients_label.label = label; + this.recipients_button.tooltip_text = tooltip; } internal void set_mode(Widget.PresentationMode mode) { @@ -77,8 +81,9 @@ public class Composer.Headerbar : Hdy.HeaderBar { break; } - this.show_close_button = (mode == Widget.PresentationMode.PANED - && this.config.desktop_environment != UNITY); + //XXX GTK4 still need to figure out close buttons + // this.show_close_button = (mode == Widget.PresentationMode.PANED + // && this.config.desktop_environment != UNITY); } private void set_attached(bool is_attached) { diff --git a/src/client/composer/composer-link-popover.vala b/src/client/composer/composer-link-popover.vala index c4bc3d96..cb4d3a2e 100644 --- a/src/client/composer/composer-link-popover.vala +++ b/src/client/composer/composer-link-popover.vala @@ -83,9 +83,9 @@ public class Composer.LinkPopover : Gtk.Popover { this.url.grab_focus(); } - public override void destroy() { + public override void dispose() { this.validation_timeout.reset(); - base.destroy(); + base.dispose(); } public void set_link_url(string url) { @@ -129,25 +129,24 @@ public class Composer.LinkPopover : Gtk.Popover { } } - Gtk.StyleContext style = this.url.get_style_context(); Gtk.EntryIconPosition pos = Gtk.EntryIconPosition.SECONDARY; if (!is_valid) { - style.add_class(Gtk.STYLE_CLASS_ERROR); - style.remove_class(Gtk.STYLE_CLASS_WARNING); + this.url.add_css_class("error"); + this.url.remove_css_class("warning"); this.url.set_icon_from_icon_name(pos, "dialog-error-symbolic"); this.url.set_tooltip_text( _("Link URL is not correctly formatted, e.g. http://example.com") ); } else if (!is_nominal) { - style.remove_class(Gtk.STYLE_CLASS_ERROR); - style.add_class(Gtk.STYLE_CLASS_WARNING); + this.url.remove_css_class("error"); + this.url.add_css_class("warning"); this.url.set_icon_from_icon_name(pos, "dialog-warning-symbolic"); this.url.set_tooltip_text( !is_mailto ? _("Invalid link URL") : _("Invalid email address") ); } else { - style.remove_class(Gtk.STYLE_CLASS_ERROR); - style.remove_class(Gtk.STYLE_CLASS_WARNING); + this.url.remove_css_class("error"); + this.url.remove_css_class("warning"); this.url.set_icon_from_icon_name(pos, null); this.url.set_tooltip_text(""); } diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala index abba0d22..b1469bad 100644 --- a/src/client/composer/composer-web-view.vala +++ b/src/client/composer/composer-web-view.vala @@ -78,7 +78,7 @@ public class Composer.WebView : Components.WebView { public Gdk.RGBA font_color { get; private set; - default = Util.Gtk.rgba(0, 0, 0, 1); + default = { 0, 0, 0, 1 }; } private uint context = 0; @@ -141,10 +141,8 @@ public class Composer.WebView : Components.WebView { public signal void image_file_dropped(string filename, string type, uint8[] contents); - public WebView(Application.Configuration config) { - base(config); - - add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK); + public WebView(Application.Configuration config, GLib.File cache_dir) { + base(config, cache_dir); this.user_content_manager.add_style_sheet(WebView.app_style); this.user_content_manager.add_script(WebView.app_script); @@ -248,11 +246,16 @@ public class Composer.WebView : Components.WebView { * Pastes plain text from the clipboard into the view. */ public void paste_plain_text() { - get_clipboard(Gdk.SELECTION_CLIPBOARD).request_text((clipboard, text) => { - if (text != null) { + var clipboard = get_clipboard(); + clipboard.read_text_async.begin(null, (obj, res) => { + try { + string text = clipboard.read_text_async.end(res); + if (text != null) insert_text(text); - } - }); + } catch (Error err) { + debug("Couldn't read text from clipboard to paste: %s", err.message); + } + }); } /** diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 66a8c3e8..c4c09695 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -18,7 +18,7 @@ private errordomain AttachmentError { * Container}. */ [GtkTemplate (ui = "/org/gnome/Geary/composer-widget.ui")] -public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { +public class Composer.Widget : Adw.Bin, Geary.BaseInterface { /** @@ -115,73 +115,19 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private enum DraftPolicy { DISCARD, KEEP } - private class HeaderRow : Gtk.Box, Geary.BaseInterface { - - - static construct { - set_css_name("geary-composer-widget-header-row"); - } - - public Gtk.Label label { get; private set; } - public Gtk.Box value_container { get; private set; } - public T value { get; private set; } - - - public HeaderRow(string label, T value) { - Object(orientation: Gtk.Orientation.HORIZONTAL); - base_ref(); - - this.label = new Gtk.Label(label); - this.label.use_underline = true; - this.label.xalign = 1.0f; - add(this.label); - - this.value_container = new Gtk.Box(HORIZONTAL, 0); - this.value_container.get_style_context().add_class("linked"); - add(this.value_container); - - this.value = value; - - var value_widget = value as Gtk.Widget; - if (value_widget != null) { - value_widget.hexpand = true; - this.value_container.add(value_widget); - this.label.set_mnemonic_widget(value_widget); - } - - show_all(); - } - - ~HeaderRow() { - base_unref(); - } - - } - - private class EntryHeaderRow : HeaderRow { - - - public Components.EntryUndo? undo { get; private set; } - - - public EntryHeaderRow(string label, T value) { - base(label, value); - var value_entry = value as Gtk.Entry; - if (value_entry != null) { - this.undo = new Components.EntryUndo(value_entry); - } - } - - } - - private class FromAddressMap { + private class FromAddressMap : GLib.Object { public Application.AccountContext account; public Geary.RFC822.MailboxAddresses from; + public FromAddressMap(Application.AccountContext account, Geary.RFC822.MailboxAddresses from) { this.account = account; this.from = from; } + + public bool is_primary() { + return this.account.account.information.primary_mailbox == this.from[0]; + } } // XXX need separate composer close action in addition to the @@ -193,25 +139,20 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private const string ACTION_ADD_ATTACHMENT = "add-attachment"; private const string ACTION_ADD_ORIGINAL_ATTACHMENTS = "add-original-attachments"; private const string ACTION_CLOSE = "composer-close"; - private const string ACTION_CUT = "cut"; private const string ACTION_DETACH = "detach"; private const string ACTION_DISCARD = "discard"; - private const string ACTION_PASTE = "paste"; private const string ACTION_SEND = "send"; private const string ACTION_SHOW_EXTENDED_HEADERS = "show-extended-headers"; private const ActionEntry[] ACTIONS = { - { Action.Edit.COPY, on_copy }, { Action.Window.CLOSE, on_close }, { Action.Window.SHOW_HELP_OVERLAY, on_show_help_overlay }, { Action.Window.SHOW_MENU, on_show_window_menu }, { ACTION_ADD_ATTACHMENT, on_add_attachment }, { ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments }, { ACTION_CLOSE, on_close }, - { ACTION_CUT, on_cut }, { ACTION_DETACH, on_detach }, { ACTION_DISCARD, on_discard }, - { ACTION_PASTE, on_paste }, { ACTION_SEND, on_send }, { ACTION_SHOW_EXTENDED_HEADERS, on_toggle_action, null, "false", on_show_extended_headers_toggled }, @@ -220,14 +161,15 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { static construct { set_css_name("geary-composer-widget"); + + typeof(AddressesRow).ensure(); + typeof(FromAddressMap).ensure(); } public static void add_accelerators(Application.Client application) { application.add_window_accelerators(ACTION_DISCARD, { "Escape" } ); application.add_window_accelerators(ACTION_ADD_ATTACHMENT, { "t" } ); application.add_window_accelerators(ACTION_DETACH, { "d" } ); - application.add_window_accelerators(ACTION_CUT, { "x" } ); - application.add_window_accelerators(ACTION_PASTE, { "v" } ); } private const string DRAFT_SAVED_TEXT = _("Saved"); @@ -270,11 +212,11 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** Determines if the composer is completely empty. */ public bool is_blank { get { - return this.to_row.value.is_empty - && this.cc_row.value.is_empty - && this.bcc_row.value.is_empty - && this.reply_to_row.value.is_empty - && this.subject_row.value.buffer.length == 0 + return this.to_row.is_empty + && this.cc_row.is_empty + && this.bcc_row.is_empty + && this.reply_to_row.is_empty + && this.subject_row.text.length == 0 && this.editor.body.is_empty && this.attached_files.size == 0; } @@ -309,32 +251,32 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** Current text of the `to` entry. */ public string to { - get { return this.to_row.value.get_text(); } - private set { this.to_row.value.set_text(value); } + get { return this.to_row.text; } + private set { this.to_row.text = value; } } /** Current text of the `cc` entry. */ public string cc { - get { return this.cc_row.value.get_text(); } - private set { this.cc_row.value.set_text(value); } + get { return this.cc_row.text; } + private set { this.cc_row.text = value; } } /** Current text of the `bcc` entry. */ public string bcc { - get { return this.bcc_row.value.get_text(); } - private set { this.bcc_row.value.set_text(value); } + get { return this.bcc_row.text; } + private set { this.bcc_row.text = value; } } /** Current text of the `reply-to` entry. */ public string reply_to { - get { return this.reply_to_row.value.get_text(); } - private set { this.reply_to_row.value.set_text(value); } + get { return this.reply_to_row.text; } + private set { this.reply_to_row.text = value; } } /** Current text of the `sender` entry. */ public string subject { - get { return this.subject_row.value.get_text(); } - private set { this.subject_row.value.set_text(value); } + get { return this.subject_row.text; } + private set { this.subject_row.text = value; } } /** The In-Reply-To header value for the composed email, if any. */ @@ -350,7 +292,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** Overrides for the draft folder as save destination, if any. */ internal Geary.Folder? save_to { get; private set; default = null; } - internal Headerbar header { get; private set; } + internal Composer.Header header { get; private set; } internal bool has_multiple_from_addresses { get { @@ -362,27 +304,26 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } [GtkChild] private unowned Gtk.Box header_container; - [GtkChild] private unowned Gtk.Grid editor_container; + [GtkChild] private unowned Adw.Bin editor_bin; - [GtkChild] private unowned Gtk.Grid email_headers; - [GtkChild] private unowned Gtk.Box filled_headers; + [GtkChild] private unowned Gtk.Box email_headers; + [GtkChild] private unowned Gtk.ListBox filled_headers; [GtkChild] private unowned Gtk.Revealer extended_headers_revealer; - [GtkChild] private unowned Gtk.Box extended_headers; + [GtkChild] private unowned Gtk.ListBox extended_headers; [GtkChild] private unowned Gtk.ToggleButton show_extended_headers; - private Gee.ArrayList from_list = new Gee.ArrayList(); + [GtkChild] private unowned Adw.ComboRow from_row; + [GtkChild] private unowned GLib.ListStore from_model; - private Gtk.SizeGroup header_labels_group = new Gtk.SizeGroup(HORIZONTAL); + [GtkChild] private unowned AddressesRow to_row; + [GtkChild] private unowned AddressesRow cc_row; + [GtkChild] private unowned AddressesRow bcc_row; + [GtkChild] private unowned AddressesRow reply_to_row; + [GtkChild] private unowned Adw.EntryRow subject_row; - private HeaderRow from_row; - private HeaderRow to_row; - private HeaderRow cc_row; - private HeaderRow bcc_row; - private HeaderRow reply_to_row; - private HeaderRow subject_row; - - private Gspell.Checker subject_spell_checker = new Gspell.Checker(null); - private Gspell.Entry subject_spell_entry; + private Spelling.Checker subject_spell_checker = new Spelling.Checker(null, null); + // XXX GTK4 probably needs to become a SourceBuffer + // private Gtk.Entry subject_spell_entry; [GtkChild] private unowned Gtk.Box attachments_box; [GtkChild] private unowned Gtk.Box hidden_on_attachment_drag_over; @@ -455,83 +396,35 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { Application.Configuration config, Application.AccountContext initial_account, Geary.Folder? save_to = null) { - components_reflow_box_get_type(); + //XXX GTK4 + // components_reflow_box_get_type(); base_ref(); this.application = application; this.config = config; this.sender_context = initial_account; this.save_to = save_to; - this.header = new Headerbar(config); + this.header = new Header(config); this.header.expand_composer.connect(on_expand_compact_headers); // Hide until we know we can save drafts this.header.show_save_and_close = false; // Setup drag 'n drop - const Gtk.TargetEntry[] target_entries = { { URI_LIST_MIME_TYPE, 0, 0 } }; - Gtk.drag_dest_set(this, Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT, - target_entries, Gdk.DragAction.COPY); - - add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK); - this.visible_on_attachment_drag_over.remove( this.visible_on_attachment_drag_over_child ); - this.from_row = new HeaderRow( - /// Translators: Label for composer From address entry - _("_From"), new Gtk.ComboBoxText() - ); - this.from_row.value.changed.connect(on_envelope_changed); - var cells = this.from_row.value.get_cells(); - ((Gtk.CellRendererText) cells.data).ellipsize = END; - this.header_labels_group.add_widget(this.from_row.label); - this.filled_headers.add(this.from_row); + this.from_row.notify["selected"].connect(on_envelope_changed); - this.to_row = new EntryHeaderRow( - /// Translators: Label for composer To address entry - _("_To"), new EmailEntry(this) - ); - this.to_row.value_container.add(this.show_extended_headers); - this.to_row.value.changed.connect(on_envelope_changed); - this.header_labels_group.add_widget(this.to_row.label); - this.filled_headers.add(this.to_row); + this.to_row.changed.connect(on_envelope_changed); + this.cc_row.changed.connect(on_envelope_changed); + this.bcc_row.changed.connect(on_envelope_changed); + this.reply_to_row.changed.connect(on_envelope_changed); - this.cc_row = new EntryHeaderRow( - /// Translators: Label for composer CC address entry - _("_Cc"), new EmailEntry(this) - ); - this.cc_row.value.changed.connect(on_envelope_changed); - this.header_labels_group.add_widget(this.cc_row.label); - this.extended_headers.add(this.cc_row); - - this.bcc_row = new EntryHeaderRow( - /// Translators: Label for composer BCC address entry - _("_Bcc"), new EmailEntry(this) - ); - this.bcc_row.value.changed.connect(on_envelope_changed); - this.header_labels_group.add_widget(this.bcc_row.label); - this.extended_headers.add(this.bcc_row); - - this.reply_to_row = new EntryHeaderRow( - /// Translators: Label for composer Reply-To address entry - _("_Reply to"), new EmailEntry(this) - ); - this.reply_to_row.value.changed.connect(on_envelope_changed); - this.header_labels_group.add_widget(this.reply_to_row.label); - this.extended_headers.add(this.reply_to_row); - - this.subject_row = new EntryHeaderRow( - /// Translators: Label for composer Subject line entry - _("_Subject"), new Gtk.Entry() - ); - this.subject_row.value.changed.connect(on_subject_changed); - this.header_labels_group.add_widget(this.subject_row.label); - this.email_headers.add(this.subject_row); - - this.subject_spell_entry = Gspell.Entry.get_from_gtk_entry( - this.subject_row.value - ); + //XXX GTK4 + // this.subject_spell_entry = Gspell.Entry.get_from_gtk_entry( + // this.subject_row.value + // ); config.settings.changed[ Application.Configuration.SPELL_CHECK_LANGUAGES ].connect(() => { @@ -539,7 +432,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { }); update_subject_spell_checker(); - this.editor = new Editor(config); + this.editor = new Editor(config, application.get_web_cache_dir()); this.editor.insert_image.connect( (from_clipboard) => { if (from_clipboard) { @@ -551,9 +444,10 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { ); this.editor.body.content_loaded.connect(on_content_loaded); this.editor.body.document_modified.connect(() => { draft_changed(); }); - this.editor.body.key_press_event.connect(on_editor_key_press_event); - this.editor.show(); - this.editor_container.add(this.editor); + Gtk.EventControllerKey editor_key_controller = new Gtk.EventControllerKey(); + editor_key_controller.key_pressed.connect(on_editor_key_pressed); + this.editor.body.add_controller(editor_key_controller); + this.editor_bin.child = this.editor; // Listen to account signals to update from menu. this.application.account_available.connect( @@ -588,7 +482,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // window actions. But for some reason, we can't use the same // prefix for the headerbar. insert_action_group(Action.Window.GROUP_NAME, this.actions); - this.header.insert_action_group("cmh", this.actions); + this.header.headerbar.insert_action_group("cmh", this.actions); validate_send_button(); load_entry_completions(); @@ -727,16 +621,16 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.from = full_context.from; } if (full_context.to != null) { - this.to_row.value.addresses = full_context.to; + this.to_row.addresses = full_context.to; } if (full_context.cc != null) { - this.cc_row.value.addresses = full_context.cc; + this.cc_row.addresses = full_context.cc; } if (full_context.bcc != null) { - this.bcc_row.value.addresses = full_context.bcc; + this.bcc_row.addresses = full_context.bcc; } if (full_context.reply_to != null) { - this.reply_to_row.value.addresses = full_context.reply_to; + this.reply_to_row.addresses = full_context.reply_to; } if (full_context.in_reply_to != null) { this.in_reply_to = this.in_reply_to.concatenate_list( @@ -853,7 +747,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // window then focus that, else focus something useful. bool refocus = true; if (focused_widget != null) { - Window? focused_window = focused_widget.get_toplevel() as Window; + Window? focused_window = focused_widget.get_root() as Window; if (new_window == focused_window) { focused_widget.grab_focus(); refocus = false; @@ -875,8 +769,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { * The return value specifies whether the composer is being closed * or if the prompt was cancelled by a human. */ - public CloseStatus conditional_close(bool should_prompt, - bool is_shutdown = false) { + public async CloseStatus conditional_close(bool should_prompt, + bool is_shutdown = false) { CloseStatus status = CLOSED; switch (this.current_mode) { case PresentationMode.CLOSED: @@ -896,44 +790,39 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } else if (should_prompt) { present(); if (this.can_save) { - var dialog = new TernaryConfirmationDialog( - this.container.top_window, + var dialog = new Adw.AlertDialog( // Translators: This dialog text is displayed to the // user when closing a composer where the options are // Keep, Discard or Cancel. _("Do you want to keep or discard this draft message?"), - null, - Stock._KEEP, - Stock._DISCARD, Gtk.ResponseType.CLOSE, - "", - is_shutdown ? "destructive-action" : "", - Gtk.ResponseType.OK // Default == Keep - ); - Gtk.ResponseType response = dialog.run(); - if (response == CANCEL || - response == DELETE_EVENT) { - // Cancel + null); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("keep", _("_Keep")); + dialog.add_response("discard", _("_Discard")); + dialog.default_response = "keep"; + dialog.close_response = "cancel"; + if (is_shutdown) + dialog.set_response_appearance("discard", DESTRUCTIVE); + string response = yield dialog.choose(this.container.top_window, null); + if (response == "cancel") { status = CANCELLED; - } else if (response == OK) { - // Keep + } else if (response == "keep") { this.save_and_close.begin(); - } else { - // Discard + } else { // Discard this.discard_and_close.begin(); } } else { - AlertDialog dialog = new ConfirmationDialog( - container.top_window, + var dialog = new Adw.AlertDialog( // Translators: This dialog text is displayed to the // user when closing a composer where the options are // only Discard or Cancel. _("Do you want to discard this draft message?"), - null, - Stock._DISCARD, - "" - ); - Gtk.ResponseType response = dialog.run(); - if (response == OK) { + null); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("discard", _("_Discard")); + dialog.close_response = "cancel"; + string response = yield dialog.choose(this.container.top_window, null); + if (response == "discard") { this.discard_and_close.begin(); } else { status = CANCELLED; @@ -981,7 +870,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - public override void destroy() { + public override void dispose() { if (this.draft_manager != null) { warning("Draft manager still open on composer destroy"); } @@ -992,7 +881,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.application.account_unavailable.disconnect( on_account_unavailable ); - base.destroy(); + base.dispose(); } /** @@ -1007,7 +896,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // Need to update this separately since it may be detached // from the widget itself. - this.header.set_sensitive(enabled); + this.header.headerbar.set_sensitive(enabled); if (enabled) { var current_account = this.sender_context.account; @@ -1044,10 +933,11 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { */ private void load_entry_completions() { Application.ContactStore contacts = this.sender_context.contacts; - this.to_row.value.completion = new ContactEntryCompletion(contacts); - this.cc_row.value.completion = new ContactEntryCompletion(contacts); - this.bcc_row.value.completion = new ContactEntryCompletion(contacts); - this.reply_to_row.value.completion = new ContactEntryCompletion(contacts); + + this.to_row.contacts = contacts; + this.cc_row.contacts = contacts; + this.bcc_row.contacts = contacts; + this.reply_to_row.contacts = contacts; } /** @@ -1105,35 +995,38 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.context_type = REPLY_ALL; } - if (!this.to_row.value.addresses.contains_all(to_addresses)) { - this.to_row.value.set_modified(); - } - if (!this.cc_row.value.addresses.contains_all(cc_addresses)) { - this.cc_row.value.set_modified(); - } - if (this.bcc != "") { - this.bcc_row.value.set_modified(); - } + //XXX GTK4 + // if (!this.to_row.addresses.contains_all(to_addresses)) { + // this.to_row.value.set_modified(); + // } + // if (!this.cc_row.addresses.contains_all(cc_addresses)) { + // this.cc_row.value.set_modified(); + // } + // if (this.bcc != "") { + // this.bcc_row.value.set_modified(); + // } // We're in compact inline mode, but there are modified email // addresses, so set us to use plain inline mode instead so // the modified addresses can be seen. If there are CC - if (this.current_mode == INLINE_COMPACT && ( - this.to_row.value.is_modified || - this.cc_row.value.is_modified || - this.bcc_row.value.is_modified || - this.reply_to_row.value.is_modified)) { - set_mode(INLINE); - } + //XXX GTK4 + // if (this.current_mode == INLINE_COMPACT && ( + // // this.to_row.value.is_modified || + // this.cc_row.value.is_modified || + // this.bcc_row.value.is_modified || + // this.reply_to_row.value.is_modified)) { + // set_mode(INLINE); + // } // If there's a modified header that would normally be hidden, // show full fields. - if (this.bcc_row.value.is_modified || - this.reply_to_row.value.is_modified) { - this.actions.change_action_state( - ACTION_SHOW_EXTENDED_HEADERS, true - ); - } + //XXX GTK4 + // if (this.bcc_row.value.is_modified || + // this.reply_to_row.value.is_modified) { + // this.actions.change_action_state( + // ACTION_SHOW_EXTENDED_HEADERS, true + // ); + // } } } @@ -1148,9 +1041,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.current_mode != INLINE_COMPACT ); if (not_inline && Geary.String.is_empty(to)) { - this.to_row.value.grab_focus(); + this.to_row.grab_focus(); } else if (not_inline && Geary.String.is_empty(subject)) { - this.subject_row.value.grab_focus(); + this.subject_row.grab_focus(); } else { // Need to grab the focus after the content has finished // loading otherwise the text caret will not be visible. @@ -1198,82 +1091,49 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { if (visible) { int height = hidden_on_attachment_drag_over.get_allocated_height(); this.hidden_on_attachment_drag_over.remove(this.hidden_on_attachment_drag_over_child); - this.visible_on_attachment_drag_over.pack_start(this.visible_on_attachment_drag_over_child, true, true); + this.visible_on_attachment_drag_over.append(this.visible_on_attachment_drag_over_child); this.visible_on_attachment_drag_over.set_size_request(-1, height); } else { - this.hidden_on_attachment_drag_over.add(this.hidden_on_attachment_drag_over_child); + this.hidden_on_attachment_drag_over.append(this.hidden_on_attachment_drag_over_child); this.visible_on_attachment_drag_over.remove(this.visible_on_attachment_drag_over_child); this.visible_on_attachment_drag_over.set_size_request(-1, -1); } } [GtkCallback] - private void on_set_focus_child() { - var window = get_toplevel() as Gtk.Window; - if (window != null) { - Gtk.Widget? last_focused = window.get_focus(); - if (last_focused == this.editor.body || - (last_focused is Gtk.Entry && last_focused.is_ancestor(this))) { - this.focused_input_widget = last_focused; - } - } - } - - [GtkCallback] - private bool on_drag_motion() { + private Gdk.DragAction on_drop_target_enter(Gtk.DropTarget drop_target, + double x, + double y) { show_attachment_overlay(true); - return false; + return Gdk.DragAction.COPY; } [GtkCallback] - private void on_drag_leave() { + private void on_drop_target_leave(Gtk.DropTarget drop_target) { show_attachment_overlay(false); } [GtkCallback] - private void on_drag_data_received(Gtk.Widget sender, Gdk.DragContext context, int x, int y, - Gtk.SelectionData selection_data, uint info, uint time_) { - - bool dnd_success = false; - if (selection_data.get_length() >= 0) { - dnd_success = true; - - string uri_list = (string) selection_data.get_data(); - string[] uris = uri_list.strip().split("\n"); - foreach (string uri in uris) { - if (!uri.has_prefix(FILE_URI_PREFIX)) - continue; - - try { - add_attachment_part(File.new_for_uri(uri.strip())); - draft_changed(); - } catch (Error err) { - attachment_failed(err.message); - } + private bool on_drop_target_drop(Gtk.DropTarget drop_target, Value val, double x, double y) { + //XXX GTK4 - I'm not sure if this is 100% correct, so best to check + if (val.holds(typeof(GLib.File))) { + var file = val as GLib.File; + try { + add_attachment_part(file); + draft_changed(); + return true; + } catch (Error err) { + attachment_failed(err.message); } } - Gtk.drag_finish(context, dnd_success, false, time_); + return false; } [GtkCallback] - private bool on_drag_drop(Gtk.Widget sender, Gdk.DragContext context, int x, int y, uint time_) { - if (context.list_targets() == null) - return false; - - uint length = context.list_targets().length(); - Gdk.Atom? target_type = null; - for (uint i = 0; i < length; i++) { - Gdk.Atom target = context.list_targets().nth_data(i); - if (target.name() == URI_LIST_MIME_TYPE) - target_type = target; - } - - if (target_type == null) - return false; - - Gtk.drag_get_data(sender, context, target_type, time_); - return true; + private bool on_drop_target_accept(Gtk.DropTarget drop_target, Gdk.Drop drop) { + //XXX GTK4 I'm not sure if this is 100% correct + return drop.formats.contain_mime_type(URI_LIST_MIME_TYPE); } /** Returns a representation of the current message. */ @@ -1283,13 +1143,13 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { date_override ?? new DateTime.now_local(), from ).set_to( - this.to_row.value.addresses + this.to_row.addresses ).set_cc( - this.cc_row.value.addresses + this.cc_row.addresses ).set_bcc( - this.bcc_row.value.addresses + this.bcc_row.addresses ).set_reply_to( - this.reply_to_row.value.addresses + this.reply_to_row.addresses ).set_subject( this.subject ).set_in_reply_to( @@ -1351,8 +1211,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.sender_context.account.information.sender_mailboxes; // Add the sender to the To address list if needed - this.to_row.value.addresses = Geary.RFC822.Utils.merge_addresses( - this.to_row.value.addresses, + this.to_row.addresses = Geary.RFC822.Utils.merge_addresses( + this.to_row.addresses, Geary.RFC822.Utils.create_to_addresses_for_reply( referred, sender_addresses ) @@ -1360,14 +1220,14 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { if (type == REPLY_ALL) { // Add other recipients to the Cc address list if needed, // but don't include any already in the To list. - this.cc_row.value.addresses = Geary.RFC822.Utils.remove_addresses( + this.cc_row.addresses = Geary.RFC822.Utils.remove_addresses( Geary.RFC822.Utils.merge_addresses( - this.cc_row.value.addresses, + this.cc_row.addresses, Geary.RFC822.Utils.create_cc_addresses_for_reply_all( referred, sender_addresses ) ), - this.to_row.value.addresses + this.to_row.addresses ); } @@ -1387,11 +1247,13 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.referred_ids.add(referred.id); } - public override bool key_press_event(Gdk.EventKey event) { + [GtkCallback] + private bool on_key_pressed(uint keyval, uint keycode, Gdk.ModifierType state) { + // XXX GTK4 check if this actually prevents the default handler from running // Override the method since key-press-event is run last, and // we want this behaviour to take precedence over the default // key handling - return check_send_on_return(event) && base.key_press_event(event); + return check_send_on_return(keyval, state); } /** Updates the composer's top level window and headerbar title. */ @@ -1443,15 +1305,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } internal void embed_header() { - if (this.header.parent == null) { - this.header_container.add(this.header); - this.header.hexpand = true; - } - } - - internal void free_header() { - if (this.header.parent != null) { - this.header.parent.remove(this.header); + if (this.header.headerbar.parent == null) { + this.header_container.append(this.header.headerbar); + this.header.headerbar.hexpand = true; } } @@ -1517,9 +1373,14 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } if (confirmation != null) { - ConfirmationDialog dialog = new ConfirmationDialog(container.top_window, - confirmation, null, Stock._OK, "suggested-action"); - return (dialog.run() == Gtk.ResponseType.OK); + var dialog = new Adw.AlertDialog(confirmation, null); + dialog.add_response("cancel", _("_Cancel")); + dialog.add_response("ok", _("_OK")); + dialog.close_response = "cancel"; + dialog.default_response = "ok"; + dialog.set_response_appearance("ok", Adw.ResponseAppearance.SUGGESTED); + string response = yield dialog.choose(this.container.top_window, null); + return (response == "ok"); } return true; } @@ -1778,7 +1639,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private void update_attachments_view() { if (this.attached_files.size > 0 ) - attachments_box.show_all(); + attachments_box.show(); else attachments_box.hide(); } @@ -1860,24 +1721,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } Gtk.Box wrapper_box = new Gtk.Box(VERTICAL, 0); - this.attachments_box.pack_start(wrapper_box); - wrapper_box.pack_start(new Gtk.Separator(HORIZONTAL)); + this.attachments_box.append(wrapper_box); + wrapper_box.append(new Gtk.Separator(HORIZONTAL)); Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); - wrapper_box.pack_start(box); + wrapper_box.append(box); /// In the composer, the filename followed by its filesize, i.e. "notes.txt (1.12KB)" string label_text = _("%s (%s)").printf(target.get_basename(), Files.get_filesize_as_string(target_info.get_size())); Gtk.Label label = new Gtk.Label(label_text); - box.pack_start(label); + box.append(label); label.halign = Gtk.Align.START; label.ellipsize = Pango.EllipsizeMode.MIDDLE; label.has_tooltip = true; label.query_tooltip.connect(Util.Gtk.query_tooltip_label); - Gtk.Button remove_button = new Gtk.Button.from_icon_name("user-trash-symbolic", BUTTON); - box.pack_start(remove_button, false, false); + Gtk.Button remove_button = new Gtk.Button.from_icon_name("user-trash-symbolic"); + box.append(remove_button); remove_button.clicked.connect(() => remove_attachment(target, wrapper_box)); update_attachments_view(); @@ -1961,19 +1822,22 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void attachment_failed(string msg) { - ErrorDialog dialog = new ErrorDialog(this.container.top_window, _("Cannot add attachment"), msg); - dialog.run(); + var dialog = new Adw.AlertDialog(_("Cannot add attachment"), msg); + dialog.add_css_class("error"); + dialog.present(this.container.top_window); } private void remove_attachment(File file, Gtk.Box box) { if (!this.attached_files.remove(file)) return; - foreach (weak Gtk.Widget child in this.attachments_box.get_children()) { + unowned Gtk.Widget? child = this.attachments_box.get_first_child(); + while (child != null) { if (child == box) { this.attachments_box.remove(box); break; } + child = child.get_next_sibling(); } update_attachments_view(); @@ -1989,33 +1853,30 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // requesting the image from the clipboard this.editor.start_background_work_pulse(); - get_clipboard(Gdk.SELECTION_CLIPBOARD).request_image((clipboard, pixbuf) => { - if (pixbuf != null) { - MemoryOutputStream os = new MemoryOutputStream(null); - pixbuf.save_to_stream_async.begin(os, "png", null, (obj, res) => { - try { - pixbuf.save_to_stream_async.end(res); - os.close(); - - Geary.Memory.ByteBuffer byte_buffer = new Geary.Memory.ByteBuffer.from_memory_output_stream(os); - - GLib.DateTime time_now = new GLib.DateTime.now(); - string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash()); - - string unique_filename; - add_inline_part(byte_buffer, filename, out unique_filename); - this.editor.body.insert_image( - Components.WebView.INTERNAL_URL_PREFIX + unique_filename - ); - } catch (Error error) { - this.application.report_problem( - new Geary.ProblemReport(error) - ); - } - + var clipboard = get_clipboard(); + clipboard.read_texture_async.begin(null, (obj, res) => { + try { + var texture = clipboard.read_texture_async.end(res); + if (texture == null) { + warning("Failed to get image from clipboard"); this.editor.stop_background_work_pulse(); - }); - } else { + } + + var png_bytes = texture.save_to_png_bytes(); + uint8[] png_data = png_bytes.get_data(); + Geary.Memory.ByteBuffer byte_buffer = new Geary.Memory.ByteBuffer.take(png_data, png_data.length); + + GLib.DateTime time_now = new GLib.DateTime.now(); + string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash()); + + string unique_filename; + add_inline_part(byte_buffer, filename, out unique_filename); + this.editor.body.insert_image( + Components.WebView.INTERNAL_URL_PREFIX + unique_filename + ); + + this.editor.stop_background_work_pulse(); + } catch (Error err) { warning("Failed to get image from clipboard"); this.editor.stop_background_work_pulse(); } @@ -2026,19 +1887,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { * Handle prompting for an inserting images as inline attachments */ private void insert_image() { - AttachmentDialog dialog = new AttachmentDialog( - this.container.top_window, this.config - ); + var dialog = new Gtk.FileDialog(); Gtk.FileFilter filter = new Gtk.FileFilter(); // Translators: This is the name of the file chooser filter // when inserting an image in the composer. - filter.set_name(_("Images")); + filter.set_filter_name(_("Images")); filter.add_mime_type("image/*"); - dialog.add_filter(filter); - if (dialog.run() == Gtk.ResponseType.ACCEPT) { - dialog.hide(); - foreach (File file in dialog.get_files()) { - try { + var filters = new ListStore(typeof(Gtk.FileFilter)); + filters.append(filter); + dialog.set_filters(filters); + + dialog.open_multiple.begin(this.container.top_window, null, (obj, res) => { + try { + ListModel? files = dialog.open_multiple.end(res); + if (files == null) + return; + + for (uint i = 0; i < files.get_n_items(); i++) { + File file = (File) files.get_item(i); check_attachment_file(file); Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true); string path = file.get_path(); @@ -2047,24 +1913,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.editor.body.insert_image( Components.WebView.INTERNAL_URL_PREFIX + unique_filename ); - } catch (Error err) { - attachment_failed(err.message); - break; } + } catch (Gtk.DialogError.DISMISSED err) { + debug("Composer: image insert canceled by user"); + } catch (GLib.Error err) { + attachment_failed(err.message); } - } - dialog.destroy(); + }); } - private bool check_send_on_return(Gdk.EventKey event) { + private bool check_send_on_return(uint keyval, Gdk.ModifierType state) { bool ret = Gdk.EVENT_PROPAGATE; - switch (Gdk.keyval_name(event.keyval)) { + switch (Gdk.keyval_name(keyval)) { case "Return": case "KP_Enter": // always trap Ctrl+Enter/Ctrl+KeypadEnter to prevent // the Enter leaking through to the controls, but only // send if send is available - if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) { + if (Gdk.ModifierType.CONTROL_MASK in state) { this.actions.activate_action(ACTION_SEND, null); ret = Gdk.EVENT_STOP; } @@ -2078,41 +1944,41 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // fields must be either empty or valid. get_action(ACTION_SEND).set_enabled( this.can_send && - this.to_row.value.is_valid && - (this.cc_row.value.is_empty || this.cc_row.value.is_valid) && - (this.bcc_row.value.is_empty || this.bcc_row.value.is_valid) && - (this.reply_to_row.value.is_empty || this.reply_to_row.value.is_valid) + this.to_row.is_valid && + (this.cc_row.is_empty || this.cc_row.is_valid) && + (this.bcc_row.is_empty || this.bcc_row.is_valid) && + (this.reply_to_row.is_empty || this.reply_to_row.is_valid) ); this.header.show_send = this.can_send; } private void set_compact_header_recipients() { - bool tocc = !this.to_row.value.is_empty && !this.cc_row.value.is_empty, - ccbcc = !(this.to_row.value.is_empty && this.cc_row.value.is_empty) && !this.bcc_row.value.is_empty; - string label = this.to_row.value.buffer.text + (tocc ? ", " : "") - + this.cc_row.value.buffer.text + (ccbcc ? ", " : "") + this.bcc_row.value.buffer.text; + bool tocc = !this.to_row.is_empty && !this.cc_row.is_empty, + ccbcc = !(this.to_row.is_empty && this.cc_row.is_empty) && !this.bcc_row.is_empty; + string label = this.to_row.text + (tocc ? ", " : "") + + this.cc_row.text + (ccbcc ? ", " : "") + this.bcc_row.text; StringBuilder tooltip = new StringBuilder(); - if (this.to_row.value.addresses != null) { - foreach(Geary.RFC822.MailboxAddress addr in this.to_row.value.addresses) { + if (this.to_row.addresses != null) { + foreach(Geary.RFC822.MailboxAddress addr in this.to_row.addresses) { // Translators: Human-readable version of the RFC 822 To header tooltip.append("%s %s\n".printf(_("To:"), addr.to_full_display())); } } - if (this.cc_row.value.addresses != null) { - foreach(Geary.RFC822.MailboxAddress addr in this.cc_row.value.addresses) { + if (this.cc_row.addresses != null) { + foreach(Geary.RFC822.MailboxAddress addr in this.cc_row.addresses) { // Translators: Human-readable version of the RFC 822 CC header tooltip.append("%s %s\n".printf(_("Cc:"), addr.to_full_display())); } } - if (this.bcc_row.value.addresses != null) { - foreach(Geary.RFC822.MailboxAddress addr in this.bcc_row.value.addresses) { + if (this.bcc_row.addresses != null) { + foreach(Geary.RFC822.MailboxAddress addr in this.bcc_row.addresses) { // Translators: Human-readable version of the RFC 822 BCC header tooltip.append("%s %s\n".printf(_("Bcc:"), addr.to_full_display())); } } - if (this.reply_to_row.value.addresses != null) { - foreach(Geary.RFC822.MailboxAddress addr in this.reply_to_row.value.addresses) { + if (this.reply_to_row.addresses != null) { + foreach(Geary.RFC822.MailboxAddress addr in this.reply_to_row.addresses) { // Translators: Human-readable version of the RFC 822 Reply-To header tooltip.append("%s%s\n".printf(_("Reply-To: "), addr.to_full_display())); } @@ -2120,56 +1986,37 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.header.set_recipients(label, tooltip.str.slice(0, -1)); // Remove trailing \n } - private void on_cut(SimpleAction action, Variant? param) { - var editable = this.container.get_focus() as Gtk.Editable; - if (editable != null) { - editable.cut_clipboard(); - } - } - - private void on_copy(SimpleAction action, Variant? param) { - var editable = this.container.get_focus() as Gtk.Editable; - if (editable != null) { - editable.copy_clipboard(); - } - } - - private void on_paste(SimpleAction action, Variant? param) { - var editable = this.container.get_focus() as Gtk.Editable; - if (editable != null) { - editable.paste_clipboard(); - } - } - private void on_toggle_action(SimpleAction? action, Variant? param) { action.change_state(!action.state.get_boolean()); } - private void reparent_widget(Gtk.Widget child, Gtk.Container new_parent) { - ((Gtk.Container) child.get_parent()).remove(child); - new_parent.add(child); + private void reparent_box_child(Gtk.Widget child, Gtk.ListBox new_parent) { + // Explicitly take a ref here so it doesn't get accidentally destroyed + Gtk.Widget child_copy = child; + ((Gtk.ListBox) child.parent).remove(child); + new_parent.append(child); } private void update_extended_headers(bool reorder=true) { - bool cc = !this.cc_row.value.is_empty; - bool bcc = !this.bcc_row.value.is_empty; - bool reply_to = !this.reply_to_row.value.is_empty; + bool cc = !this.cc_row.is_empty; + bool bcc = !this.bcc_row.is_empty; + bool reply_to = !this.reply_to_row.is_empty; if (reorder) { if (cc) { - reparent_widget(this.cc_row, this.filled_headers); + reparent_box_child(this.cc_row, this.filled_headers); } else { - reparent_widget(this.cc_row, this.extended_headers); + reparent_box_child(this.cc_row, this.extended_headers); } if (bcc) { - reparent_widget(this.bcc_row, this.filled_headers); + reparent_box_child(this.bcc_row, this.filled_headers); } else { - reparent_widget(this.bcc_row, this.extended_headers); + reparent_box_child(this.bcc_row, this.extended_headers); } if (reply_to) { - reparent_widget(this.reply_to_row, this.filled_headers); + reparent_box_child(this.reply_to_row, this.filled_headers); } else { - reparent_widget(this.reply_to_row, this.extended_headers); + reparent_box_child(this.reply_to_row, this.extended_headers); } } @@ -2190,19 +2037,23 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - private bool on_editor_key_press_event(Gdk.EventKey event) { + private bool on_editor_key_pressed(Gtk.EventControllerKey controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { + bool is_modifier = ((Gdk.KeyEvent) controller.get_current_event()).is_modifier(); // Widget's keypress override doesn't receive non-modifier // keys when the editor processes them, regardless if true or // false is called; this deals with that issue (specifically // so Ctrl+Enter will send the message) - if (event.is_modifier == 0) { - if (check_send_on_return(event) == Gdk.EVENT_STOP) + if (!is_modifier) { + if (check_send_on_return(keyval, state) == Gdk.EVENT_STOP) return Gdk.EVENT_STOP; } if (this.can_delete_quote) { this.can_delete_quote = false; - if (event.is_modifier == 0 && event.keyval == Gdk.Key.BackSpace) { + if (!is_modifier && keyval == Gdk.Key.BackSpace) { this.editor.body.delete_quoted_message(); return Gdk.EVENT_STOP; } @@ -2215,38 +2066,55 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return this.actions.lookup_action(action_name) as GLib.SimpleAction; } - private bool add_account_emails_to_from_list( + private bool add_account_emails_to_from_model( Application.AccountContext other_account, bool set_active = false ) { bool is_primary = true; Geary.AccountInformation info = other_account.account.information; foreach (Geary.RFC822.MailboxAddress mailbox in info.sender_mailboxes) { - Geary.RFC822.MailboxAddresses addresses = - new Geary.RFC822.MailboxAddresses.single(mailbox); + var addresses = new Geary.RFC822.MailboxAddresses.single(mailbox); + var addr_map = new FromAddressMap(other_account, addresses); - string display = mailbox.to_full_display(); - if (!is_primary) { - // Displayed in the From dropdown to indicate an - // "alternate email address" for an account. The first - // printf argument will be the alternate email address, - // and the second will be the account's primary email - // address. - display = _("%1$s via %2$s").printf(display, info.display_name); - } - is_primary = false; - - this.from_row.value.append_text(display); - this.from_list.add(new FromAddressMap(other_account, addresses)); + this.from_model.append(addr_map); if (!set_active && this.from.equal_to(addresses)) { - this.from_row.value.set_active(this.from_list.size - 1); + this.from_row.selected = this.from_model.get_n_items() - 1; set_active = true; } } return set_active; } + [GtkCallback] + private void on_from_factory_setup(Gtk.SignalListItemFactory factory, + GLib.Object object) { + unowned var item = (Gtk.ListItem) object; + var label = new Gtk.Label(null); + item.child = label; + } + + [GtkCallback] + private void on_from_factory_bind(Gtk.SignalListItemFactory factory, + GLib.Object object) { + unowned var item = (Gtk.ListItem) object; + unowned var addr_map = (FromAddressMap) item.item; + + unowned var label = (Gtk.Label) item.child; + + string display = addr_map.from.to_full_display(); + if (!addr_map.is_primary()) { + // Displayed in the From dropdown to indicate an + // "alternate email address" for an account. The first + // printf argument will be the alternate email address, + // and the second will be the account's primary email + // address. + var info = addr_map.account.account.information; + display = _("%1$s via %2$s").printf(display, info.display_name); + } + label.label = display; + } + private void update_info_label() { string text = ""; if (this.can_delete_quote) { @@ -2261,7 +2129,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // the from address had to be set private bool update_from_field() { this.from_row.visible = false; - this.from_row.value.changed.disconnect(on_from_changed); // Don't show in inline unless the current account has // multiple email accounts or aliases, since these will be replies to a @@ -2285,16 +2152,14 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } this.from_row.visible = true; - this.from_row.value.remove_all(); - this.from_list = new Gee.ArrayList(); // Always add at least the current account. The var set_active // is set to true if the current message's from address has // been set in the ComboBox. - bool set_active = add_account_emails_to_from_list(this.sender_context); + bool set_active = add_account_emails_to_from_model(this.sender_context); foreach (var account in accounts) { if (account != this.sender_context) { - set_active = add_account_emails_to_from_list( + set_active = add_account_emails_to_from_model( account, set_active ); } @@ -2304,17 +2169,16 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { // The identity or account that was active before has been // removed use the best we can get now (primary address of // the account or any other) - this.from_row.value.set_active(0); + this.from_row.selected = 0; } - this.from_row.value.changed.connect(on_from_changed); + this.from_row.notify["selected"].connect((obj, pspec) => { on_from_changed(); }); return !set_active; } private void update_from() throws Error { - int index = this.from_row.value.get_active(); - if (index >= 0) { - FromAddressMap selected = this.from_list.get(index); + FromAddressMap selected = this.from_row.selected_item as FromAddressMap; + if (selected != null) { this.from = selected.from; if (selected.account != this.sender_context) { @@ -2362,8 +2226,10 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void update_subject_spell_checker() { - Gspell.Language? lang = null; + Spelling.Language? lang = null; string[] langs = this.config.get_spell_check_languages(); + //XXX GTK4 +#if 0 if (langs.length == 1) { lang = Gspell.Language.lookup(langs[0]); } else { @@ -2395,13 +2261,14 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { var buffer = Gspell.EntryBuffer.get_from_gtk_entry_buffer( this.subject_row.value.buffer ); - Gspell.Checker checker = null; + Spelling.Checker checker = null; if (lang != null) { checker = this.subject_spell_checker; checker.language = lang; } this.subject_spell_entry.inline_spell_checking = (checker != null); buffer.spell_checker = checker; +#endif } private void on_draft_id_changed() { @@ -2416,7 +2283,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { update_draft_state(); } - private void on_subject_changed() { + [GtkCallback] + private void on_subject_changed(Gtk.Editable subject_editable) { draft_changed(); update_window_title(); } @@ -2444,23 +2312,24 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void on_add_attachment() { - AttachmentDialog dialog = new AttachmentDialog( - this.container.top_window, this.config - ); - if (dialog.run() == Gtk.ResponseType.ACCEPT) { - dialog.hide(); - foreach (File file in dialog.get_files()) { - try { + var dialog = new Gtk.FileDialog(); + dialog.open_multiple.begin(this.container.top_window, null, (obj, res) => { + try { + ListModel? files = dialog.open_multiple.end(res); + if (files == null) + return; + + for (uint i = 0; i < files.get_n_items(); i++) { + File file = (File) files.get_item(i); add_attachment_part(file); draft_changed(); - } catch (Error err) { - attachment_failed(err.message); - break; } + } catch (Gtk.DialogError.DISMISSED err) { + debug("Composer: opening attachment canceled by user"); + } catch (Error err) { + attachment_failed(err.message); } - - } - dialog.destroy(); + }); } private void on_pending_attachments() { @@ -2470,7 +2339,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void on_close() { - conditional_close(this.container is Window); + conditional_close.begin(this.container is Window); } private void on_show_window_menu() { @@ -2491,7 +2360,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private void on_discard() { if (this.container is Window) { - conditional_close(true); + conditional_close.begin(true); } else { this.discard_and_close.begin(); } diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index 12669865..c9b51414 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -35,7 +35,7 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { public Window(Widget composer, Application.Client application) { - Object(application: application, type: Gtk.WindowType.TOPLEVEL); + Object(application: application); this.composer = composer; this.composer.set_mode(DETACHED); @@ -47,42 +47,34 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { // XXX Bug 764622 set_property("name", "GearyComposerWindow"); - add(this.composer); + this.child = this.composer; this.composer.update_window_title(); if (application.config.desktop_environment == UNITY) { composer.embed_header(); } else { - set_titlebar(this.composer.header); + set_titlebar(this.composer.header.headerbar); } - this.focus_in_event.connect((w, e) => { + Gtk.EventControllerFocus focus_controller = new Gtk.EventControllerFocus(); + focus_controller.enter.connect((controller) => { application.controller.window_focus_in(); - return false; }); - this.focus_out_event.connect((w, e) => { + focus_controller.leave.connect((controller) => { application.controller.window_focus_out(); - return false; }); - - show(); - set_position(Gtk.WindowPosition.CENTER); + ((Gtk.Widget) this).add_controller(focus_controller); } /** {@inheritDoc} */ public new void close() { - this.composer.free_header(); - remove(this.composer); - destroy(); + this.child = null; } public override void show() { Gdk.Display? display = Gdk.Display.get_default(); if (display != null) { - Gdk.Monitor? monitor = display.get_primary_monitor(); - if (monitor == null) { - monitor = display.get_monitor_at_point(1, 1); - } + Gdk.Monitor? monitor = display.get_monitor_at_surface(get_surface()); int[] size = this.application.config.get_composer_window_size(); //check if stored values are reasonable if (monitor != null && @@ -98,44 +90,36 @@ public class Composer.Window : Gtk.ApplicationWindow, Container { } private void save_window_geometry () { - if (!this.is_maximized) { + if (!this.maximized) { Gdk.Display? display = get_display(); - Gdk.Window? window = get_window(); - if (display != null && window != null) { - Gdk.Monitor monitor = display.get_monitor_at_window(window); - - int width = 0; - int height = 0; - get_size(out width, out height); + Gdk.Surface? surface = get_surface(); + if (display != null && surface != null) { + Gdk.Monitor monitor = display.get_monitor_at_surface(surface); // Only store if the values are reasonable-looking. - if (width > 0 && width <= monitor.geometry.width && - height > 0 && height <= monitor.geometry.height) { + if (this.default_width > 0 && this.default_width <= monitor.geometry.width && + this.default_height > 0 && this.default_height <= monitor.geometry.height) { this.application.config.set_composer_window_size({ - width, height + this.default_width, this.default_height }); } } } } - // Fired on window resize. Save window size for the next start. - public override void size_allocate(Gtk.Allocation allocation) { - base.size_allocate(allocation); + public override bool close_request() { + save_window_geometry(); - this.save_window_geometry(); - } - - public override bool delete_event(Gdk.EventAny event) { // Use the child instead of the `composer` property so we // don't check with the composer if it has already been // removed from the container. Widget? child = get_child() as Widget; bool ret = Gdk.EVENT_PROPAGATE; - if (child != null && - child.conditional_close(true) == CANCELLED) { - ret = Gdk.EVENT_STOP; - } + // XXX GTK4 - This is now an async method, I'm not sure we can still stop htis? + // if (child != null && + // child.conditional_close(true) == CANCELLED) { + // ret = Gdk.EVENT_STOP; + // } return ret; } diff --git a/src/client/composer/contact-entry-completion.vala b/src/client/composer/contact-entry-completion.vala index 4d6a350a..ccdda790 100644 --- a/src/client/composer/contact-entry-completion.vala +++ b/src/client/composer/contact-entry-completion.vala @@ -6,7 +6,9 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface { +public class ContactEntryCompletion : Adw.EntryRow, Geary.BaseInterface { + //XXX GTK4 probably want to create a ContactEntryRow or something like that, GtkEntryCompletion is deprecated +#if 0 // Minimum visibility for the contact to appear in autocompletion. @@ -365,4 +367,5 @@ public class ContactEntryCompletion : Gtk.EntryCompletion, Geary.BaseInterface { return true; } +#endif } diff --git a/src/client/composer/spell-check-popover.vala b/src/client/composer/spell-check-popover.vala index d7f2bdf2..fe5be39e 100644 --- a/src/client/composer/spell-check-popover.vala +++ b/src/client/composer/spell-check-popover.vala @@ -60,8 +60,10 @@ public class SpellCheckPopover { this.is_lang_visible = is_active || is_visible; Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); - box.margin = 6; box.margin_start = 12; + box.margin_end = 6; + box.margin_top = 6; + box.margin_bottom = 6; lang_name = Util.I18n.language_name_from_locale(lang_code); country_name = Util.I18n.country_name_from_locale(lang_code); @@ -80,28 +82,27 @@ public class SpellCheckPopover { country_label.halign = Gtk.Align.START; country_label.ellipsize = END; country_label.xalign = 0; - country_label.get_style_context().add_class("dim-label"); + country_label.add_css_class("dim-label"); - label_box.add(label); - label_box.add(country_label); - box.pack_start(label_box, false, false); + label_box.append(label); + label_box.append(country_label); + box.append(label_box); } else { - box.pack_start(label, false, false); + box.append(label); } - Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; - active_image = new Gtk.Image.from_icon_name("object-select-symbolic", sz); + active_image = new Gtk.Image.from_icon_name("object-select-symbolic"); this.visibility_button = new Gtk.Button(); - this.visibility_button.set_relief(Gtk.ReliefStyle.NONE); - box.pack_start(active_image, false, false, 6); - box.pack_start(this.visibility_button, true, true); + this.visibility_button.add_css_class("flat"); + box.append(active_image); + box.append(this.visibility_button); this.visibility_button.halign = Gtk.Align.END; // Make the button stay at the right end of the screen this.visibility_button.valign = CENTER; this.visibility_button.clicked.connect(on_visibility_clicked); update_images(); - add(box); + this.child = box; } public bool is_lang_active() { @@ -109,23 +110,21 @@ public class SpellCheckPopover { } private void update_images() { - Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; - switch (lang_active) { case SpellCheckStatus.ACTIVE: - active_image.set_from_icon_name("object-select-symbolic", sz); + this.active_image.set_from_icon_name("object-select-symbolic"); break; case SpellCheckStatus.INACTIVE: - active_image.clear(); + this.active_image.clear(); break; } if (is_lang_visible) { - this.visibility_button.set_image(new Gtk.Image.from_icon_name("list-remove-symbolic", sz)); + this.visibility_button.icon_name = "list-remove-symbolic"; this.visibility_button.set_tooltip_text(_("Remove this language from the preferred list")); } else { - this.visibility_button.set_image(new Gtk.Image.from_icon_name("list-add-symbolic", sz)); + this.visibility_button.icon_name = "list-add-symbolic"; this.visibility_button.set_tooltip_text(_("Add this language to the preferred list")); } } @@ -193,7 +192,7 @@ public class SpellCheckPopover { } public SpellCheckPopover(Gtk.MenuButton button, Application.Configuration config) { - this.popover = new Gtk.Popover(button); + this.popover = new Gtk.Popover(); button.popover = this.popover; this.config = config; this.selected_rows = new GLib.GenericSet(GLib.str_hash, GLib.str_equal); @@ -224,12 +223,11 @@ public class SpellCheckPopover { search_box = new Gtk.SearchEntry(); search_box.set_placeholder_text(_("Search for more languages")); search_box.changed.connect(on_search_box_changed); - search_box.grab_focus.connect(on_search_box_grab_focus); - content.pack_start(search_box, false, true); + content.append(search_box); - view = new Gtk.ScrolledWindow(null, null); - view.set_shadow_type(Gtk.ShadowType.IN); + view = new Gtk.ScrolledWindow(); view.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + view.vexpand = true; langs_list = new Gtk.ListBox(); langs_list.set_selection_mode(Gtk.SelectionMode.NONE); @@ -239,7 +237,7 @@ public class SpellCheckPopover { lang in enabled_langs, lang in visible_langs ); - langs_list.add(row); + langs_list.append(row); if (row.is_lang_active()) selected_rows.add(lang); @@ -248,14 +246,14 @@ public class SpellCheckPopover { row.visibility_changed.connect(this.on_row_visibility_changed); } langs_list.row_activated.connect(on_row_activated); - view.add(langs_list); + view.child = langs_list; - content.pack_start(view, true, true); + content.append(view); langs_list.set_filter_func(this.filter_function); langs_list.set_header_func(this.header_function); - popover.add(content); + this.popover.child = content; // Make sure that the search box does not get the focus first. We want it to have it only // if the user wants to perform an extended search. @@ -265,8 +263,8 @@ public class SpellCheckPopover { content.set_margin_top(6); content.set_margin_bottom(6); - popover.show.connect(this.on_shown); - popover.set_size_request(360, 350); + this.popover.show.connect(this.on_shown); + this.popover.set_size_request(360, 350); } private void on_row_activated(Gtk.ListBoxRow row) { @@ -281,10 +279,6 @@ public class SpellCheckPopover { langs_list.invalidate_filter(); } - private void on_search_box_grab_focus() { - set_expanded(true); - } - private void set_expanded(bool expanded) { is_expanded = expanded; langs_list.invalidate_filter(); @@ -295,8 +289,6 @@ public class SpellCheckPopover { content.set_focus_child(view); is_expanded = false; langs_list.invalidate_filter(); - - popover.show_all(); } private void on_row_enabled_changed(SpellCheckLangRow row, diff --git a/src/client/conversation-list/conversation-list-row.vala b/src/client/conversation-list/conversation-list-row.vala index d6d231b9..c7bc9c7a 100644 --- a/src/client/conversation-list/conversation-list-row.vala +++ b/src/client/conversation-list/conversation-list-row.vala @@ -110,10 +110,10 @@ internal class ConversationList.Row : Gtk.ListBoxRow { private void set_button_active(bool active) { this.selected_button.set_active(active); if (active) { - this.get_style_context().add_class("selected"); + this.add_css_class("selected"); this.set_state_flags(Gtk.StateFlags.SELECTED, false); } else { - this.get_style_context().remove_class("selected"); + this.remove_css_class("selected"); this.unset_state_flags(Gtk.StateFlags.SELECTED); } } @@ -134,9 +134,9 @@ internal class ConversationList.Row : Gtk.ListBoxRow { private void update_flags(Geary.Email? email) { if (conversation.is_unread()) { - get_style_context().add_class("unread"); + add_css_class("unread"); } else { - get_style_context().remove_class("unread"); + remove_css_class("unread"); } if (conversation.is_flagged()) { diff --git a/src/client/conversation-list/conversation-list-view.vala b/src/client/conversation-list/conversation-list-view.vala index 499ac996..39573e11 100644 --- a/src/client/conversation-list/conversation-list-view.vala +++ b/src/client/conversation-list/conversation-list-view.vala @@ -11,7 +11,8 @@ * */ [GtkTemplate (ui = "/org/gnome/Geary/conversation-list-view.ui")] -public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { +public class ConversationList.View : Adw.Bin, Geary.BaseInterface { + /** * The fields that must be available on any ConversationMonitor * passed to ConversationList.View @@ -42,11 +43,11 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { private Application.Configuration config; - private Gtk.GestureMultiPress press_gesture; private Gtk.GestureLongPress long_press_gesture; - private Gtk.EventControllerKey key_event_controller; private Gdk.ModifierType last_modifier_type; + [GtkChild] public unowned Gtk.ScrolledWindow scrolled_window; + [GtkChild] private unowned Gtk.ListBox list; /* @@ -64,14 +65,10 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { this.list.set_header_func(header_func); - this.vadjustment.value_changed.connect(maybe_load_more); - this.vadjustment.value_changed.connect(update_visible_conversations); + this.scrolled_window.vadjustment.value_changed.connect(maybe_load_more); + this.scrolled_window.vadjustment.value_changed.connect(update_visible_conversations); - this.press_gesture = new Gtk.GestureMultiPress(this.list); - this.press_gesture.set_button(0); - this.press_gesture.released.connect(on_press_gesture_released); - - this.long_press_gesture = new Gtk.GestureLongPress(this.list); + this.long_press_gesture = new Gtk.GestureLongPress(); this.long_press_gesture.propagation_phase = CAPTURE; this.long_press_gesture.pressed.connect((n_press, x, y) => { Row? row = (Row) this.list.get_row_at_y((int) y); @@ -80,14 +77,13 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { this.selection_mode_enabled = true; } }); + this.list.add_controller(this.long_press_gesture); - this.key_event_controller = new Gtk.EventControllerKey(this.list); - this.key_event_controller.key_pressed.connect(on_key_event_controller_key_pressed); - - Gtk.drag_source_set(this.list, Gdk.ModifierType.BUTTON1_MASK, FolderList.Tree.TARGET_ENTRY_LIST, - Gdk.DragAction.COPY | Gdk.DragAction.MOVE); - this.list.drag_begin.connect(on_drag_begin); - this.list.drag_end.connect(on_drag_end); + //XXX GTK4 - check if started on click + var drag_source = new Gtk.DragSource(); + drag_source.drag_begin.connect(on_drag_begin); + drag_source.drag_end.connect(on_drag_end); + this.list.add_controller(drag_source); } static construct { @@ -113,10 +109,13 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { * automatically but instead it must be externally scheduled */ public void refresh_times() { - this.list.foreach((child) => { - var row = (Row) child; + int i = 0; + Row? row = this.list.get_row_at_index(0) as Row; + while (row != null) { row.refresh_time(); - }); + i++; + row = this.list.get_row_at_index(i) as Row; + } } // ------------------- @@ -204,7 +203,7 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { private Gtk.Popover construct_popover(Row row, uint selection_size) { GLib.Menu context_menu_model = new GLib.Menu(); - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { if (!main.is_shift_down) { @@ -303,13 +302,8 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { ); context_menu_model.append_section(null, actions_section); - // Use a popover rather than a regular context menu since - // the latter grabs the event queue, so the MainWindow - // will not receive events if the user releases Shift, - // making the trash/delete header bar state wrong. - Gtk.Popover context_menu = new Gtk.Popover.from_model( - row, context_menu_model - ); + var context_menu = new Gtk.PopoverMenu.from_model(context_menu_model); + context_menu.set_parent(row); return context_menu; } @@ -347,13 +341,16 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { * If a conversation is not present in the ListBox, it is ignored. */ public void select_conversations(Gee.Collection selection) { - this.list.foreach((child) => { - var row = (Row) child; + int i = 0; + Row? row = this.list.get_row_at_index(0) as Row; + while (row != null) { Geary.App.Conversation conversation = row.conversation; if (selection.contains(conversation)) { this.list.select_row(row); } - }); + i++; + row = this.list.get_row_at_index(i) as Row; + } } /** @@ -466,9 +463,9 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { */ private int VISIBILITY_UPDATE_DELAY_MS = 1000; - /** - * The set of all conversations currently displayed in the viewport - */ + /** + * The set of all conversations currently displayed in the viewport + */ public Gee.Set visible_conversations {get; private set; default = new Gee.HashSet(); } private Geary.Scheduler.Scheduled? scheduled_visible_update; @@ -482,7 +479,7 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { scheduled_visible_update = Geary.Scheduler.after_msec(VISIBILITY_UPDATE_DELAY_MS, () => { var visible = new Gee.HashSet(); - Gtk.ListBoxRow? first = this.list.get_row_at_y((int) this.vadjustment.value); + Gtk.ListBoxRow? first = this.list.get_row_at_y((int) this.scrolled_window.vadjustment.value); if (first == null) { this.visible_conversations = visible; @@ -492,7 +489,7 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { uint start_index = ((uint) first.get_index()); uint end_index = uint.min( // Assume that all messages are the same height - start_index + (uint) (this.vadjustment.page_size / first.get_allocated_height()), + start_index + (uint) (this.scrolled_window.vadjustment.page_size / first.get_allocated_height()), this.model.get_n_items() ); @@ -581,29 +578,34 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { * Update conversation row */ private void on_conversation_updated(Geary.App.Conversation convo) { - this.list.foreach((child) => { - var row = (Row) child; + int i = 0; + Row? row = this.list.get_row_at_index(0) as Row; + while (row != null) { if (convo == row.conversation) { row.update(); } - }); + + i++; + row = this.list.get_row_at_index(i) as Row; + } } // ---------- // Gestures // ---------- - private void on_press_gesture_released(int n_press, double x, double y) { + [GtkCallback] + private void on_press_gesture_released(Gtk.GestureClick click_gesture, int n_press, double x, double y) { var row = (Row) this.list.get_row_at_y((int) y); if (row == null) return; - var button = this.press_gesture.get_current_button(); - if (button == 1) { - Gdk.EventSequence sequence = this.press_gesture.get_current_sequence(); - Gdk.Event event = this.press_gesture.get_last_event(sequence); - event.get_state(out this.last_modifier_type); + var button = click_gesture.get_current_button(); + if (button == Gdk.BUTTON_PRIMARY) { + Gdk.EventSequence sequence = click_gesture.get_current_sequence(); + Gdk.Event event = click_gesture.get_last_event(sequence); + this.last_modifier_type = event.get_modifier_state(); if (!this.selection_mode_enabled) { if ((this.last_modifier_type & Gdk.ModifierType.SHIFT_MASK) == Gdk.ModifierType.SHIFT_MASK || @@ -614,19 +616,19 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { conversation_activated(((Row) row).conversation, 1); } } - } else if (button == 2) { + } else if (button == Gdk.BUTTON_MIDDLE) { conversation_activated(row.conversation, 2); - } else if (button == 3) { - var rect = Gdk.Rectangle(); - row.translate_coordinates(this.list, 0, 0, out rect.x, out rect.y); - rect.x = (int) x; - rect.y = (int) y - rect.y; - rect.width = rect.height = 0; + } else if (button == Gdk.BUTTON_SECONDARY) { + Graphene.Point p = { (float) x, (float) y }; + Graphene.Point p_row; + this.list.compute_point(row, p, out p_row); + Gdk.Rectangle rect = { (int) p_row.x, (int) p_row.y, 0, 0 }; context_menu(row, rect); } } - private bool on_key_event_controller_key_pressed(uint keyval, uint keycode, Gdk.ModifierType modifier_type) { + [GtkCallback] + private bool on_key_pressed(uint keyval, uint keycode, Gdk.ModifierType modifier_type) { switch (keyval) { case Gdk.Key.Up: case Gdk.Key.Down: @@ -646,19 +648,17 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { } - /** - * Widgets used as drag icons have to be explicitly destroyed after the drag - * so we track the widget as a private member - */ + /** + * Widgets used as drag icons have to be explicitly destroyed after the drag + * so we track the widget as a private member + */ private Row? drag_widget = null; - private void on_drag_begin(Gdk.DragContext ctx) { + private void on_drag_begin(Gtk.DragSource drag_source, Gdk.Drag drag) { int screen_x, screen_y; Gdk.ModifierType _modifier; - this.get_window().get_device_position(ctx.get_device(), out screen_x, out screen_y, out _modifier); - - Row? row = this.list.get_row_at_y(screen_y + (int) this.vadjustment.value) as Row?; + Row? row = this.list.get_row_at_y((int) this.scrolled_window.vadjustment.value) as Row?; if (row != null) { // If the user has a selection but drags starting from an unselected // row, we need to set the selection to that row @@ -669,18 +669,18 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { this.drag_widget = new Row(this.config, row.conversation, false); this.drag_widget.width_request = row.get_allocated_width(); - this.drag_widget.get_style_context().add_class("drag-n-drop"); + this.drag_widget.add_css_class("drag-n-drop"); this.drag_widget.visible = true; - int hot_x, hot_y; - this.translate_coordinates(row, screen_x, screen_y, out hot_x, out hot_y); - Gtk.drag_set_icon_widget(ctx, this.drag_widget, hot_x, hot_y); + double hot_x, hot_y; + // XXX GTK4 - this might be a bit more work to do properly wiht Paintable + translate_coordinates(row, 0, 0, out hot_x, out hot_y); + // drag_source.set_icon(this.drag_widget, hot_x, hot_y); } } - private void on_drag_end(Gdk.DragContext ctx) { + private void on_drag_end(Gtk.DragSource drag_source, Gdk.Drag drag, bool delete_data) { if (this.drag_widget != null) { - this.drag_widget.destroy(); this.drag_widget = null; } } @@ -706,10 +706,13 @@ public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface { } private void on_selection_mode_changed() { - this.list.foreach((child) => { - var row = (Row) child; + int i = 0; + Row? row = this.list.get_row_at_index(0) as Row; + while (row != null) { row.set_selection_enabled(this.selection_mode_enabled); - }); + i++; + row = this.list.get_row_at_index(i) as Row; + } if (this.selection_mode_enabled) { this.to_restore_row = this.list.get_selected_row(); diff --git a/src/client/conversation-viewer/conversation-contact-popover.vala b/src/client/conversation-viewer/conversation-contact-popover.vala index 0c69d654..075a05a3 100644 --- a/src/client/conversation-viewer/conversation-contact-popover.vala +++ b/src/client/conversation-viewer/conversation-contact-popover.vala @@ -43,9 +43,9 @@ public class Conversation.ContactPopover : Gtk.Popover { private Application.Configuration config; - [GtkChild] private unowned Gtk.Grid contact_pane; + [GtkChild] private unowned Gtk.Box contact_pane; - [GtkChild] private unowned Hdy.Avatar avatar; + [GtkChild] private unowned Adw.Avatar avatar; [GtkChild] private unowned Gtk.Label contact_name; @@ -55,13 +55,13 @@ public class Conversation.ContactPopover : Gtk.Popover { [GtkChild] private unowned Gtk.Button unstarred_button; - [GtkChild] private unowned Gtk.ModelButton open_button; + [GtkChild] private unowned Gtk.Button open_button; - [GtkChild] private unowned Gtk.ModelButton save_button; + [GtkChild] private unowned Gtk.Button save_button; - [GtkChild] private unowned Gtk.ModelButton load_remote_button; + [GtkChild] private unowned Gtk.CheckButton load_remote_button; - [GtkChild] private unowned Gtk.Grid deceptive_pane; + [GtkChild] private unowned Gtk.Box deceptive_pane; [GtkChild] private unowned Gtk.Label forged_email_label; @@ -79,22 +79,20 @@ public class Conversation.ContactPopover : Gtk.Popover { Geary.RFC822.MailboxAddress mailbox, Application.Configuration config) { - this.relative_to = relative_to; + set_parent(relative_to); this.contact = contact; this.mailbox = mailbox; this.config = config; - this.load_remote_button.role = CHECK; - this.contact.bind_property("display-name", this.avatar, "text", BindingFlags.SYNC_CREATE); - this.contact.bind_property("avatar", - this.avatar, - "loadable-icon", - BindingFlags.SYNC_CREATE); + load_avatar.begin((obj, res) => { load_avatar.end(res); }); + this.contact.notify["avatar"].connect((obj, pspec) => { + load_avatar.begin((obj, res) => { load_avatar.end(res); }); + }); this.actions.add_action_entries(ACTION_ENTRIES, this); insert_action_group(ACTION_GROUP, this.actions); @@ -106,10 +104,10 @@ public class Conversation.ContactPopover : Gtk.Popover { /** * Starts loading the avatar for the message's sender. */ - public override void destroy() { + public override void dispose() { this.contact.changed.disconnect(this.on_contact_changed); this.load_cancellable.cancel(); - base.destroy(); + base.dispose(); } private void update() { @@ -182,13 +180,34 @@ public class Conversation.ContactPopover : Gtk.Popover { } } + private async void load_avatar() { + if (this.contact.avatar == null) { + this.avatar.custom_image = null; + return; + } + + try { + GLib.InputStream stream = yield this.contact.avatar.load_async( + avatar.size, this.load_cancellable + ); + var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async( + stream, avatar.size, avatar.size, true, load_cancellable + ); + this.avatar.custom_image = Gdk.Texture.for_pixbuf(pixbuf); + } catch (GLib.Error err) { + debug("Couldn't load avatar for contact: %s", err.message); + this.avatar.custom_image = null; + } + } + private async void set_load_remote_resources(bool enabled) { try { // Remove all contact email domains from trusted list // Otherwise, user may not understand why images are always shown if (!enabled) { var email_addresses = this.contact.email_addresses; - foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + for (uint i = 0; i < email_addresses.get_n_items(); i++) { + var email = (Geary.RFC822.MailboxAddress) email_addresses.get_item(i); this.config.remove_images_trusted_domain(email.domain); } } @@ -214,9 +233,15 @@ public class Conversation.ContactPopover : Gtk.Popover { } private void on_copy_email() { - Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); - clipboard.set_text(this.mailbox.to_full_display(), -1); - clipboard.store(); + Gdk.Clipboard clipboard = get_clipboard(); + clipboard.set_text(this.mailbox.to_full_display()); + clipboard.store_async.begin(Priority.DEFAULT, null, (obj, res) => { + try { + clipboard.store_async.end(res); + } catch (Error err) { + debug("Couldn't copy email to clipboard: %s", err.message); + } + }); } private void on_load_remote(GLib.SimpleAction action) { @@ -225,7 +250,7 @@ public class Conversation.ContactPopover : Gtk.Popover { } private void on_new_conversation() { - var main = this.get_toplevel() as Application.MainWindow; + var main = this.get_root() as Application.MainWindow; if (main != null) { main.application.new_composer.begin(this.mailbox); } @@ -240,7 +265,7 @@ public class Conversation.ContactPopover : Gtk.Popover { } private void on_show_conversations() { - var main = this.get_toplevel() as Application.MainWindow; + var main = this.get_root() as Application.MainWindow; if (main != null) { main.show_search_bar("from:%s".printf(this.mailbox.address)); } diff --git a/src/client/conversation-viewer/conversation-email.vala b/src/client/conversation-viewer/conversation-email.vala index fddcd894..b85d2b9b 100644 --- a/src/client/conversation-viewer/conversation-email.vala +++ b/src/client/conversation-viewer/conversation-email.vala @@ -177,12 +177,12 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** Determines if the email has been manually marked as being read. */ public bool is_manually_read { - get { return get_style_context().has_class(MANUAL_READ_CLASS); } + get { return has_css_class(MANUAL_READ_CLASS); } set { if (value) { - get_style_context().add_class(MANUAL_READ_CLASS); + add_css_class(MANUAL_READ_CLASS); } else { - get_style_context().remove_class(MANUAL_READ_CLASS); + remove_css_class(MANUAL_READ_CLASS); } } } @@ -237,7 +237,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { // window, for updating email menu trash/delete actions. private bool shift_handler_installed = false; - [GtkChild] private unowned Gtk.Grid actions; + [GtkChild] private unowned Gtk.Box actions; [GtkChild] private unowned Gtk.Button attachments_button; @@ -247,7 +247,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { [GtkChild] private unowned Gtk.MenuButton email_menubutton; - [GtkChild] private unowned Gtk.Grid sub_messages; + [GtkChild] private unowned Gtk.Box sub_messages; /** Fired when a internal link is activated */ @@ -284,7 +284,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { new Geary.Nonblocking.Spinlock(load_cancellable); if (is_sent) { - get_style_context().add_class(SENT_CLASS); + add_css_class(SENT_CLASS); } // Construct the view for the primary message, hook into it @@ -295,7 +295,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.contacts, this.config ); - this.primary_message.summary.add(this.actions); + this.primary_message.summary.append(this.actions); connect_message_view_signals(this.primary_message); // Wire up the rest of the UI @@ -310,7 +310,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { BODY_LOAD_TIMEOUT_MSEC, this.on_body_loading_timeout ); - pack_start(this.primary_message, true, true, 0); + append(this.primary_message); update_email_state(); } @@ -482,7 +482,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** Displays the raw RFC 822 source for this email. */ public async void view_source() { - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { Geary.Email email = this.email; try { @@ -567,7 +567,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");"; yield this.primary_message.evaluate_javascript(js, null); - Gtk.Window? window = get_toplevel() as Gtk.Window; + Gtk.Window? window = get_root() as Gtk.Window; WebKit.PrintOperation op = this.primary_message.new_print_operation(); Gtk.PrintSettings settings = new Gtk.PrintSettings(); @@ -694,7 +694,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { Gee.List sub_messages = message.get_sub_messages(); if (sub_messages.size > 0) { - this.primary_message.body_container.add(this.sub_messages); + this.primary_message.body_container.append(this.sub_messages); } foreach (Geary.RFC822.Message sub_message in sub_messages) { ConversationMessage attached_message = @@ -706,7 +706,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { ); connect_message_view_signals(attached_message); attached_message.add_internal_resources(cid_resources); - this.sub_messages.add(attached_message); + this.sub_messages.append(attached_message); this._attached_messages.add(attached_message); attached_message.load_contacts.begin(this.load_cancellable); yield attached_message.load_message_body( @@ -719,20 +719,18 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } private void update_email_state() { - Gtk.StyleContext style = get_style_context(); - if (this.is_unread) { - style.add_class(UNREAD_CLASS); + add_css_class(UNREAD_CLASS); } else { - style.remove_class(UNREAD_CLASS); + remove_css_class(UNREAD_CLASS); } if (this.is_starred) { - style.add_class(STARRED_CLASS); + add_css_class(STARRED_CLASS); this.star_button.hide(); this.unstar_button.show(); } else { - style.remove_class(STARRED_CLASS); + remove_css_class(STARRED_CLASS); this.star_button.show(); this.unstar_button.hide(); } @@ -756,7 +754,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.conversation.base_folder is Geary.FolderSupport.Remove ); bool is_shift_down = false; - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { is_shift_down = main.is_shift_down; @@ -805,7 +803,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } ); - this.email_menubutton.popover.bind_model(new_model, null); + this.email_menubutton.menu_model = new_model; this.email_menubutton.popover.grab_focus(); } } @@ -814,13 +812,13 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private void update_displayed_attachments() { bool has_attachments = !this.displayed_attachments.is_empty; this.attachments_button.set_visible(has_attachments); - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (has_attachments && main != null) { this.attachments_pane = new Components.AttachmentPane( false, main.attachments ); - this.primary_message.body_container.add(this.attachments_pane); + this.primary_message.body_container.append(this.attachments_pane); foreach (var attachment in this.displayed_attachments) { this.attachments_pane.add_attachment( @@ -834,7 +832,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.message_body_state = FAILED; this.primary_message.show_load_error_pane(); - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { Geary.AccountInformation account = this.email_store.account.information; main.application.controller.report_problem( @@ -853,16 +851,13 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } private void activate_email_action(string name) { - GLib.ActionGroup? email_actions = get_action_group( - ConversationListBox.EMAIL_ACTION_GROUP_NAME - ); - if (email_actions != null) { - email_actions.activate_action(name, this.email.id.to_variant()); - } + //XXX GTK4 check if this works still + string action_name = ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + name; + activate_action_variant(action_name, this.email.id.to_variant()); } [GtkCallback] - private void on_email_menu() { + private void on_email_menu(Gtk.MenuButton email_menubutton) { update_email_menu(); } @@ -885,7 +880,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private void on_save_image(string uri, string? alt_text, Geary.Memory.Buffer? content) { - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; if (main != null) { if (uri.has_prefix(Components.WebView.CID_URL_PREFIX)) { string cid = uri.substring(Components.WebView.CID_URL_PREFIX.length); diff --git a/src/client/conversation-viewer/conversation-list-box.vala b/src/client/conversation-viewer/conversation-list-box.vala index 6688e271..925a9572 100644 --- a/src/client/conversation-viewer/conversation-list-box.vala +++ b/src/client/conversation-viewer/conversation-list-box.vala @@ -18,7 +18,7 @@ * ConversationListBox sorts by the {@link Geary.Email.date} field * (the Date: header), as that's the date displayed to the user. */ -public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { +public class ConversationListBox : Adw.Bin, Geary.BaseInterface { /** Fields that must be available for listing conversation email. */ public const Geary.Email.Field REQUIRED_FIELDS = ( @@ -194,17 +194,18 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { public void unmark_terms() { cancel(); - this.list.foreach((child) => { - EmailRow? row = child as EmailRow; - if (row != null) { - if (row.is_search_match) { - row.is_search_match = false; - foreach (ConversationMessage msg_view in row.view) { - msg_view.unmark_search_terms(); - } - } + for (int i = 0; true; i++) { + unowned var row = this.list.listbox.get_row_at_index(i) as EmailRow; + if (row == null) + break; + + if (row.is_search_match) { + row.is_search_match = false; + foreach (ConversationMessage msg_view in row.view) { + msg_view.unmark_search_terms(); } - }); + } + } } public void cancel() { @@ -324,58 +325,46 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // Enables firing the should_scroll signal when this row is // allocated a size public void enable_should_scroll() { - this.size_allocate.connect(on_size_allocate); + //XXX GTK4 - once we work with models, we won't need this + // this.size_allocate.connect(on_size_allocate); } private void update_css_class() { if (this.is_expanded) - get_style_context().add_class(EXPANDED_CLASS); + add_css_class(EXPANDED_CLASS); else - get_style_context().remove_class(EXPANDED_CLASS); + remove_css_class(EXPANDED_CLASS); update_previous_sibling_css_class(); } - // This is mostly taken form libhandy HdyExpanderRow - private Gtk.Widget? get_previous_sibling() { - if (this.parent is Gtk.Container) { - var siblings = this.parent.get_children(); - unowned List l; - for (l = siblings; l != null && l.next != null && l.next.data != this; l = l.next); - - if (l != null && l.next != null && l.next.data == this) { - return l.data; - } - } - - return null; - } - private void update_previous_sibling_css_class() { - var previous_sibling = get_previous_sibling(); + var previous_sibling = get_prev_sibling(); if (previous_sibling != null) { if (this.is_expanded) - previous_sibling.get_style_context().add_class("geary-expanded-previous-sibling"); + previous_sibling.add_css_class("geary-expanded-previous-sibling"); else - previous_sibling.get_style_context().remove_class("geary-expanded-previous-sibling"); + previous_sibling.remove_css_class("geary-expanded-previous-sibling"); } } protected inline void set_style_context_class(string class_name, bool value) { if (value) { - get_style_context().add_class(class_name); + add_css_class(class_name); } else { - get_style_context().remove_class(class_name); + remove_css_class(class_name); } } + //XXX GTK4 - once we work with models, we won't need this +#if 0 protected void on_size_allocate() { // Disable should_scroll so we don't keep on scrolling // later, like when the window has been resized. this.size_allocate.disconnect(on_size_allocate); should_scroll(); } - +#endif } @@ -391,7 +380,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // Does the row contain an email matching the current search? public bool is_search_match { - get { return get_style_context().has_class(MATCH_CLASS); } + get { return has_css_class(MATCH_CLASS); } set { set_style_context_class(MATCH_CLASS, value); this.is_pinned = value; @@ -407,7 +396,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { public EmailRow(ConversationEmail view) { base(view.email); this.view = view; - add(view); + this.child = view; } public override async void expand() @@ -446,14 +435,12 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { public LoadingRow() { base(null); - get_style_context().add_class(LOADING_CLASS); + add_css_class(LOADING_CLASS); Gtk.Spinner spinner = new Gtk.Spinner(); spinner.height_request = 16; spinner.width_request = 16; - spinner.show(); - spinner.start(); - add(spinner); + this.child = spinner; } } @@ -470,7 +457,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { base(view.referred); this.view = view; this.is_expanded = true; - add(this.view); + this.child = this.view; this.focus_on_click = false; } @@ -479,23 +466,13 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { static construct { - // Set up custom keybindings - unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class( - (ObjectClass) typeof(ConversationListBox).class_ref() - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.space, 0, "focus-next", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.KP_Space, 0, "focus-next", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.space, Gdk.ModifierType.SHIFT_MASK, "focus-prev", 0 - ); - Gtk.BindingEntry.add_signal( - bindings, Gdk.Key.KP_Space, Gdk.ModifierType.SHIFT_MASK, "focus-prev", 0 - ); + add_shortcut(new Gtk.Shortcut(Gtk.ShortcutTrigger.parse_string("Space"), + new Gtk.NamedAction("focus-next"))); + add_shortcut(new Gtk.Shortcut(Gtk.ShortcutTrigger.parse_string("Space"), + new Gtk.NamedAction("focus-prev"))); + //XXX GTK4 +#if 0 Gtk.BindingEntry.add_signal( bindings, Gdk.Key.Up, 0, "scroll", 1, typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_UP @@ -520,6 +497,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { bindings, Gdk.Key.End, 0, "scroll", 1, typeof(Gtk.ScrollType), Gtk.ScrollType.END ); +#endif } private static int on_sort(Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) { @@ -535,6 +513,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { return Geary.Email.compare_sent_date_ascending(email1, email2); } + internal Gtk.ListBox listbox { get; set; default = new Gtk.ListBox(); } + /** Conversation being displayed. */ public Geary.App.Conversation conversation { get; private set; } @@ -588,7 +568,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { var handled = false; var composer = this.current_composer; if (composer != null) { - var window = get_toplevel() as Gtk.Window; + var window = get_root() as Gtk.Window; if (window != null) { var focused = window.get_focus(); if (focused != null && @@ -612,7 +592,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } if (!handled) { - Gtk.Adjustment adj = get_adjustment(); + Gtk.Adjustment adj = this.listbox.get_adjustment(); double value = adj.get_value(); switch (type) { case Gtk.ScrollType.STEP_UP: @@ -645,14 +625,14 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { /** Keyboard action to shift focus to the next message, if any. */ [Signal (action=true)] public virtual signal void focus_next() { - this.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1); + this.listbox.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1, false, false); this.mark_read_timer.start(); } /** Keyboard action to shift focus to the prev message, if any. */ [Signal (action=true)] public virtual signal void focus_prev() { - this.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1); + this.listbox.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1, false, false); this.mark_read_timer.start(); } @@ -702,23 +682,20 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { MARK_READ_TIMEOUT_MSEC, this.check_mark_read ); - this.selection_mode = NONE; + this.child = this.listbox; + this.listbox.selection_mode = NONE; + this.listbox.valign = Gtk.Align.START; - get_style_context().add_class("content"); - get_style_context().add_class("background"); - get_style_context().add_class("conversation-listbox"); + this.listbox.add_css_class("content"); + this.listbox.add_css_class("conversation-listbox"); - /* we need to update the previous sibling style class when rows are added or removed */ - add.connect(update_previous_sibling_css_class); - remove.connect(update_previous_sibling_css_class); - - set_adjustment(adjustment); - set_sort_func(ConversationListBox.on_sort); + this.listbox.set_adjustment(adjustment); + this.listbox.set_sort_func(ConversationListBox.on_sort); this.email_actions.add_action_entries(email_action_entries, this); insert_action_group(EMAIL_ACTION_GROUP_NAME, this.email_actions); - this.row_activated.connect(on_row_activated); + this.listbox.row_activated.connect(on_row_activated); this.conversation.appended.connect(on_conversation_appended); this.conversation.trimmed.connect(on_conversation_trimmed); @@ -729,34 +706,45 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { base_unref(); } - public override void destroy() { + public override void dispose() { this.search.cancel(); this.cancellable.cancel(); this.email_rows.clear(); this.mark_read_timer.reset(); - base.destroy(); + base.dispose(); + } + + public void append(Gtk.Widget child) { + this.listbox.append(child); + update_previous_sibling_css_class(); + } + + public new void remove(Gtk.Widget child) { + this.listbox.remove(child); + update_previous_sibling_css_class(); } - // For some reason insert doesn't emit the add event public new void insert(Gtk.Widget child, int position) { - base.insert(child, position); + this.listbox.insert(child, position); update_previous_sibling_css_class(); } // This is mostly taken form libhandy HdyExpanderRow private void update_previous_sibling_css_class() { - var siblings = this.get_children(); - unowned List l; - for (l = siblings; l != null && l.next != null && l.next.data != this; l = l.next) { - if (l != null && l.next != null) { - var row = l.next.data as ConversationRow; - if (row != null) { - if (row.is_expanded) { - l.data.get_style_context().add_class("geary-expanded-previous-sibling"); - } else { - l.data.get_style_context().remove_class("geary-expanded-previous-sibling"); - } - } + unowned var child = get_first_child() as ConversationRow; + if (child == null) + return; + + unowned var next = child.get_next_sibling() as ConversationRow; + while (next != null) { + + child = next; + next = child.get_next_sibling() as ConversationRow; + + if (next.is_expanded) { + child.add_css_class("geary-expanded-previous-sibling"); + } else { + child.remove_css_class("geary-expanded-previous-sibling"); } } } @@ -764,7 +752,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { public async void load_conversation(Gee.Collection scroll_to, Geary.SearchQuery? query) throws GLib.Error { - set_sort_func(null); + this.listbox.set_sort_func(null); Gee.Collection? all_email = this.conversation.get_emails( Geary.App.Conversation.Ordering.SENT_DATE_ASCENDING @@ -850,7 +838,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { public void scroll_to_messages(Gee.Collection targets) { // Get the currently displayed email, allowing for some // padding at the top - Gtk.ListBoxRow? current_child = get_row_at_y(32); + Gtk.ListBoxRow? current_child = this.listbox.get_row_at_y(32); // Find the row currently at the top of the viewport EmailRow? current = null; @@ -858,7 +846,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { int pos = current_child.get_index(); do { current = current_child as EmailRow; - current_child = get_row_at_index(--pos); + current_child = this.listbox.get_row_at_index(--pos); } while (current == null && pos > 0); } @@ -904,12 +892,12 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ConversationEmail? view = get_selection_view(); if (view == null) { EmailRow? last = null; - this.foreach((child) => { - EmailRow? row = child as EmailRow; - if (row != null) { - last = row; - } - }); + for (int i = 0; true; i++) { + unowned var row = this.listbox.get_row_at_index(i) as EmailRow; + if (row == null) + break; + last = row; + } if (last != null) { view = last.view; @@ -953,7 +941,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // Use row param rather than row var from closure to avoid a // circular ref. row.should_scroll.connect((row) => { scroll_to_row(row); }); - add(row); + append(row); this.current_composer = row; embed.composer.notify["saved-id"].connect( @@ -1060,22 +1048,21 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // Since first rows may have extra margin, remove that from // the height of rows when adjusting scrolling. - Gtk.ListBoxRow initial_row = get_row_at_index(0); + Gtk.ListBoxRow initial_row = this.listbox.get_row_at_index(0); int loading_height = 0; if (initial_row is LoadingRow) { loading_height = Util.Gtk.get_border_box_height(initial_row); remove(initial_row); // Adjust for the changed margin of the first row - var first_row = get_row_at_index(0); - var style = first_row.get_style_context(); - var margin = style.get_margin(style.get_state()); + var first_row = this.listbox.get_row_at_index(0); + var margin = first_row.get_style_context().get_margin();; loading_height -= margin.top; } // None of these will be interesting, so just add them all, // but keep the scrollbar adjusted so that the first // interesting message remains visible. - Gtk.Adjustment listbox_adj = get_adjustment(); + Gtk.Adjustment listbox_adj = this.listbox.get_adjustment(); int i_mail_loaded = 0; foreach (Geary.Email email in to_insert) { EmailRow row = add_email(email, false); @@ -1096,7 +1083,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ++i_mail_loaded; } - set_sort_func(on_sort); + this.listbox.set_sort_func(on_sort); if (query != null) { // XXX this sucks for large conversations because it can take @@ -1184,19 +1171,22 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ); ConversationMessage conversation_message = view.primary_message; + // XXX GTK4 - I think we can do this separately +#if 0 conversation_message.body_container.button_release_event.connect_after((event) => { // Consume all non-consumed clicks so the row is not // inadvertently activated after clicking on the // email body. return true; }); +#endif EmailRow row = new EmailRow(view); row.email_loaded.connect((e) => { email_loaded(e); }); this.email_rows.set(email.id, row); if (append_row) { - add(row); + append(row); } else { insert(row, 0); } @@ -1222,17 +1212,17 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // Use set_value rather than clamp_value since we want to // scroll to the top of the window. - get_adjustment().set_value(y); + this.listbox.get_adjustment().set_value(y); } private void scroll_to_anchor(EmailRow row, int anchor_y) { Gtk.Allocation? alloc = null; row.get_allocation(out alloc); - int x = 0, y = 0; - row.view.primary_message.web_view_translate_coordinates(row, x, anchor_y, out x, out y); + double x = 0, y = 0; + row.view.primary_message.web_view_translate_coordinates(row, 0, anchor_y, out x, out y); - Gtk.Adjustment adj = get_adjustment(); + Gtk.Adjustment adj = this.listbox.get_adjustment(); y = alloc.y + y; adj.set_value(y); @@ -1244,15 +1234,18 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { private void check_mark_read() { Gee.List email_ids = new Gee.LinkedList(); - Gtk.Adjustment adj = get_adjustment(); + Gtk.Adjustment adj = this.listbox.get_adjustment(); int top_bound = (int) adj.value; int bottom_bound = top_bound + (int) adj.page_size; - this.foreach((child) => { + for (int i = 0; true; i++) { + unowned var row = this.listbox.get_row_at_index(i) as EmailRow; + if (row == null) + break; + // Don't bother with not-yet-loaded emails since the // size of the body will be off, affecting the visibility // of emails further down the conversation. - EmailRow? row = child as EmailRow; ConversationEmail? view = (row != null) ? row.view : null; Geary.Email? email = (view != null) ? view.email : null; if (row != null && @@ -1261,8 +1254,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { !view.is_manually_read && email.is_unread().is_certain()) { ConversationMessage conversation_message = view.primary_message; - int body_top = 0; - int body_left = 0; + double body_top = 0; + double body_left = 0; conversation_message.web_view_translate_coordinates( this, 0, 0, @@ -1270,12 +1263,12 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ); int body_height = conversation_message.web_view_get_allocated_height(); - int body_bottom = body_top + body_height; + int body_bottom = (int) body_top + body_height; // Only mark the email as read if it's actually visible if (body_height > 0 && body_bottom > top_bound && - body_top + MARK_READ_PADDING < bottom_bound) { + (int) body_top + MARK_READ_PADDING < bottom_bound) { email_ids.add(view.email.id); // Since it can take some time for the new flags @@ -1284,7 +1277,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { view.is_manually_read = true; } } - }); + } if (email_ids.size > 0) { mark_email(email_ids, null, Geary.EmailFlags.UNREAD); @@ -1401,7 +1394,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // be appended last. Finally, don't let rows with active // composers be collapsed. if (row.is_expanded) { - if (get_row_at_index(row.get_index() + 1) != null) { + if (this.listbox.get_row_at_index(row.get_index() + 1) != null) { row.collapse(); } } else { @@ -1481,15 +1474,19 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { Geary.Email email = view.email; var ids = new Gee.LinkedList(); ids.add(email.id); - this.foreach((row) => { - if (row.get_visible()) { - Geary.Email other = ((EmailRow) row).view.email; - if (Geary.Email.compare_sent_date_ascending( - email, other) < 0) { - ids.add(other.id); - } + for (int i = 0; true; i++) { + unowned var row = this.listbox.get_row_at_index(i) as EmailRow; + if (row == null) + break; + + if (row.visible) { + Geary.Email other = row.view.email; + if (Geary.Email.compare_sent_date_ascending( + email, other) < 0) { + ids.add(other.id); } - }); + } + } mark_email(ids, Geary.EmailFlags.UNREAD, null); } } diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index 4845cd05..3ae3cb81 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -15,7 +15,7 @@ * embeds at least one instance of this class. */ [GtkTemplate (ui = "/org/gnome/Geary/conversation-message.ui")] -public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { +public class ConversationMessage : Gtk.Box, Geary.BaseInterface { private const string FROM_CLASS = "geary-from"; @@ -65,9 +65,6 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private string search_value; - private Gtk.Bin container; - - public ContactFlowBoxChild(Application.Contact contact, Geary.RFC822.MailboxAddress source, Type address_type = Type.OTHER) { @@ -77,40 +74,34 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { this.search_value = source.to_searchable_string().casefold(); // Update prelight state when mouse-overed. - Gtk.EventBox events = new Gtk.EventBox(); - events.add_events( - Gdk.EventMask.ENTER_NOTIFY_MASK | - Gdk.EventMask.LEAVE_NOTIFY_MASK - ); - events.set_visible_window(false); - events.enter_notify_event.connect(on_prelight_in_event); - events.leave_notify_event.connect(on_prelight_out_event); + Gtk.EventControllerMotion controller = new Gtk.EventControllerMotion(); + controller.enter.connect(on_prelight_enter); + controller.leave.connect(on_prelight_leave); + add_controller(controller); - add(events); - this.container = events; set_halign(Gtk.Align.START); this.contact.changed.connect(on_contact_changed); update(); } - public override void destroy() { + public override void dispose() { this.contact.changed.disconnect(on_contact_changed); - base.destroy(); + base.dispose(); } public bool highlight_search_term(string term) { bool found = term in this.search_value; if (found) { - get_style_context().add_class(MATCH_CLASS); + add_css_class(MATCH_CLASS); } else { - get_style_context().remove_class(MATCH_CLASS); + remove_css_class(MATCH_CLASS); } return found; } public void unmark_search_terms() { - get_style_context().remove_class(MATCH_CLASS); + remove_css_class(MATCH_CLASS); } private void update() { @@ -120,28 +111,26 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { // both cases, but we can't yet include CSS classes in // Pango markup. See Bug 766763. - Gtk.Grid address_parts = new Gtk.Grid(); + var address_parts = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); bool is_spoofed = this.source.is_spoofed(); if (is_spoofed) { - Gtk.Image spoof_img = new Gtk.Image.from_icon_name( - "dialog-warning-symbolic", Gtk.IconSize.SMALL_TOOLBAR - ); + var spoof_img = new Gtk.Image.from_icon_name("dialog-warning-symbolic"); this.set_tooltip_text( _("This email address may have been forged") ); - address_parts.add(spoof_img); - get_style_context().add_class(SPOOF_CLASS); + address_parts.append(spoof_img); + add_css_class(SPOOF_CLASS); } Gtk.Label primary = new Gtk.Label(null); primary.ellipsize = Pango.EllipsizeMode.END; primary.set_halign(Gtk.Align.START); - primary.get_style_context().add_class(PRIMARY_CLASS); + primary.add_css_class(PRIMARY_CLASS); if (this.address_type == Type.FROM) { - primary.get_style_context().add_class(FROM_CLASS); + primary.add_css_class(FROM_CLASS); } - address_parts.add(primary); + address_parts.append(primary); string display_address = this.source.to_address_display("", ""); @@ -173,32 +162,24 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { Gtk.Label secondary = new Gtk.Label(null); secondary.ellipsize = Pango.EllipsizeMode.END; secondary.set_halign(Gtk.Align.START); - secondary.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + secondary.add_css_class("dim-label"); secondary.set_text(display_address); - address_parts.add(secondary); + address_parts.append(secondary); } - Gtk.Widget? existing_ui = this.container.get_child(); - if (existing_ui != null) { - this.container.remove(existing_ui); - } - - this.container.add(address_parts); - show_all(); + this.child = address_parts; } private void on_contact_changed() { update(); } - private bool on_prelight_in_event(Gdk.Event event) { + private void on_prelight_enter(Gtk.EventControllerMotion controller, double x, double y) { set_state_flags(Gtk.StateFlags.PRELIGHT, false); - return Gdk.EVENT_STOP; } - private bool on_prelight_out_event(Gdk.Event event) { + private void on_prelight_leave(Gtk.EventControllerMotion controller) { unset_state_flags(Gtk.StateFlags.PRELIGHT); - return Gdk.EVENT_STOP; } } @@ -207,7 +188,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { * A FlowBox that limits its contents to 12 items until a link is * clicked to expand it. Used for to, cc, and bcc fields. */ - public class ContactList : Gtk.FlowBox, Geary.BaseInterface { + public class ContactList : Adw.Bin, Geary.BaseInterface { /** * The number of results that will be displayed when not expanded. * Note this is actually one less than the cutoff, which is 12; we @@ -216,43 +197,50 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { */ private const int SHORT_RESULTS = 11; + private Gtk.FlowBox flowbox = new Gtk.FlowBox(); private Gtk.Label show_more; private Gtk.Label show_less; private bool expanded = false; private int children = 0; + public signal void child_activated(Gtk.FlowBoxChild child); construct { - this.show_more = this.create_label(); - this.show_more.activate_link.connect(() => { - this.set_expanded(true); - }); - base.add(this.show_more); + this.child = this.flowbox; + this.flowbox.column_spacing = 2; + this.flowbox.max_children_per_line = 4; + this.flowbox.selection_mode = Gtk.SelectionMode.NONE; + this.flowbox.child_activated.connect((f, c) => { child_activated(c); }); - this.show_less = this.create_label(); + this.show_more = create_label(); + this.show_more.activate_link.connect(() => { + set_expanded(true); + }); + this.flowbox.append(this.show_more); + + this.show_less = create_label(); // Translators: Label text displayed when there are too // many email addresses to be shown by default in an // email's header, but they are all being shown anyway. this.show_less.label = "%s".printf(_("Show less")); this.show_less.activate_link.connect(() => { - this.set_expanded(false); + set_expanded(false); }); - base.add(this.show_less); + this.flowbox.append(this.show_less); - this.set_filter_func(this.filter_func); + this.flowbox.set_filter_func(this.filter_func); } - public override void add(Gtk.Widget child) { + public void add(Gtk.Widget child) { // insert before the show_more and show_less labels - int length = (int) this.get_children().length(); - base.insert(child, length - 2); + this.flowbox.insert(child, n_children() - 2); this.children ++; if (this.children >= SHORT_RESULTS && this.children <= SHORT_RESULTS + 2) { - this.invalidate_filter(); + this.flowbox.invalidate_filter(); } this.show_more.label = "%s".printf( @@ -264,19 +252,27 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { ); } + private int n_children() { + int ret = 0; + unowned var child = this.flowbox.get_first_child(); + while (child != null) { + ret++; + child = child.get_next_sibling(); + } + return ret; + } private Gtk.Label create_label() { var label = new Gtk.Label(""); label.visible = true; label.use_markup = true; - label.track_visited_links = false; label.halign = START; return label; } private void set_expanded(bool expanded) { this.expanded = expanded; - this.invalidate_filter(); + this.flowbox.invalidate_filter(); } private bool filter_func(Gtk.FlowBoxChild child) { @@ -306,10 +302,10 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } /** Container for preview and full header widgets. */ - [GtkChild] internal unowned Gtk.Grid summary { get; } + [GtkChild] internal unowned Gtk.Box summary { get; } /** Container for message body components. */ - [GtkChild] internal unowned Gtk.Grid body_container { get; } + [GtkChild] internal unowned Gtk.Box body_container { get; } /** Conainer for message InfoBar widgets. */ [GtkChild] internal unowned Components.InfoBarStack info_bars { get; } @@ -338,28 +334,28 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private GLib.DateTime? local_date = null; - [GtkChild] private unowned Hdy.Avatar avatar; + [GtkChild] private unowned Adw.Avatar avatar; - [GtkChild] private unowned Gtk.Revealer compact_revealer; + [GtkChild] private unowned Gtk.Box compact_header; [GtkChild] private unowned Gtk.Label compact_from; [GtkChild] private unowned Gtk.Label compact_date; [GtkChild] private unowned Gtk.Label compact_body; - [GtkChild] private unowned Gtk.Revealer header_revealer; + [GtkChild] private unowned Gtk.Box expanded_header; [GtkChild] private unowned Gtk.FlowBox from; [GtkChild] private unowned Gtk.Label subject; private string subject_searchable = ""; [GtkChild] private unowned Gtk.Label date; - [GtkChild] private unowned Gtk.Grid sender_header; + [GtkChild] private unowned Gtk.Box sender_header; [GtkChild] private unowned Gtk.FlowBox sender_address; - [GtkChild] private unowned Gtk.Grid reply_to_header; + [GtkChild] private unowned Gtk.Box reply_to_header; [GtkChild] private unowned Gtk.FlowBox reply_to_addresses; - [GtkChild] private unowned Gtk.Grid to_header; - [GtkChild] private unowned Gtk.Grid cc_header; - [GtkChild] private unowned Gtk.Grid bcc_header; + [GtkChild] private unowned ContactList to_list; + [GtkChild] private unowned ContactList cc_list; + [GtkChild] private unowned ContactList bcc_list; [GtkChild] private unowned Gtk.Revealer body_revealer; [GtkChild] private unowned Gtk.ProgressBar body_progress; @@ -371,7 +367,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private string empty_from_label; // The web_view's context menu - private Gtk.Menu? context_menu = null; + private Gtk.PopoverMenu? context_menu = null; // Menu models for creating the context menu private MenuModel context_menu_link; @@ -549,7 +545,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { // when the message has no from address. this.empty_from_label = _("No sender"); - this.compact_from.get_style_context().add_class(FROM_CLASS); + this.compact_from.add_css_class(FROM_CLASS); if (preview != null) { string clean_preview = preview; @@ -595,10 +591,11 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { if (viewer != null && viewer.previous_web_view != null) { this.web_view = new ConversationWebView.with_related_view( this.config, + null, /// XXX GTK4 is null okay here? I honestly think not ... viewer.previous_web_view ); } else { - this.web_view = new ConversationWebView(this.config); + this.web_view = new ConversationWebView(this.config, null); /// XXX GTK4 is null okay here? I honestly think not ... } if (viewer != null) { viewer.previous_web_view = this.web_view; @@ -618,7 +615,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { this.web_view.set_hexpand(true); this.web_view.set_vexpand(true); this.web_view.show(); - this.body_container.add(this.web_view); + this.body_container.append(this.web_view); add_action(ACTION_COPY_SELECTION, false).activate.connect(() => { web_view.copy_clipboard(); }); @@ -634,13 +631,13 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { base_unref(); } - public override void destroy() { + public override void dispose() { this.show_progress_timeout.reset(); this.hide_progress_timeout.reset(); this.progress_pulse.reset(); this.resources.clear(); this.searchable_addresses.clear(); - base.destroy(); + base.dispose(); } public async string? get_selection_for_quoting() throws Error { @@ -696,10 +693,10 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { web_view.zoom_reset(); } - public void web_view_translate_coordinates(Gtk.Widget widget, int x, int anchor_y, out int x1, out int y1) { + public void web_view_translate_coordinates(Gtk.Widget widget, int x, int anchor_y, out double x1, out double y1) { if (this.web_view == null) initialize_web_view(); - web_view.translate_coordinates(widget, x, anchor_y, out x1, out y1); + this.web_view.translate_coordinates(widget, x, anchor_y, out x1, out y1); } public int web_view_get_allocated_height() { @@ -714,8 +711,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { public void show_message_body(bool include_transitions=true) { if (this.web_view == null) initialize_web_view(); - set_revealer(this.compact_revealer, false, include_transitions); - set_revealer(this.header_revealer, true, include_transitions); + this.compact_header.visible = false; + this.expanded_header.visible = true; set_revealer(this.body_revealer, true, include_transitions); } @@ -723,8 +720,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { * Hides the complete message and shows the compact headers. */ public void hide_message_body() { - compact_revealer.set_reveal_child(true); - header_revealer.set_reveal_child(false); + this.compact_header.visible = true; + this.expanded_header.visible = false; body_revealer.set_reveal_child(false); } @@ -830,7 +827,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { */ public async void load_contacts(GLib.Cancellable cancellable) throws GLib.Error { - var main = this.get_toplevel() as Application.MainWindow; + var main = this.get_root() as Application.MainWindow; if (main != null && !cancellable.is_cancelled()) { // Load the primary contact and avatar if (this.primary_originator != null) { @@ -843,10 +840,14 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { this.avatar, "text", BindingFlags.SYNC_CREATE); - this.primary_contact.bind_property("avatar", - this.avatar, - "loadable-icon", - BindingFlags.SYNC_CREATE); + + if (this.primary_contact.avatar != null) { + var icon_stream = yield this.primary_contact.avatar.load_async(48, null); + var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(icon_stream, 48, 48, true); + this.avatar.custom_image = Gdk.Texture.for_pixbuf(pixbuf); + } else { + this.avatar.custom_image = null; + } } } @@ -864,13 +865,13 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { cancellable ); yield fill_header_addresses( - this.to_header, headers.to, cancellable + this.to_list, headers.to, cancellable ); yield fill_header_addresses( - this.cc_header, headers.cc, cancellable + this.cc_list, headers.cc, cancellable ); yield fill_header_addresses( - this.bcc_header, headers.bcc, cancellable + this.bcc_list, headers.bcc, cancellable ); } } @@ -930,10 +931,10 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { string match = raw_match.casefold(); if (this.subject_searchable.contains(match)) { - this.subject.get_style_context().add_class(MATCH_CLASS); + this.subject.add_css_class(MATCH_CLASS); ++headers_found; } else { - this.subject.get_style_context().remove_class(MATCH_CLASS); + this.subject.remove_css_class(MATCH_CLASS); } foreach (ContactFlowBoxChild address in this.searchable_addresses) { @@ -1052,17 +1053,16 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { ContactFlowBoxChild.Type.FROM ); this.searchable_addresses.add(child); - this.from.add(child); + this.from.append(child); } } else { Gtk.Label label = new Gtk.Label(null); label.set_text(this.empty_from_label); Gtk.FlowBoxChild child = new Gtk.FlowBoxChild(); - child.add(label); + child.child = label; child.set_halign(Gtk.Align.START); - child.show_all(); - this.from.add(child); + this.from.append(child); } // Show the Sender header addresses if present, but only if @@ -1075,7 +1075,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { ); this.searchable_addresses.add(child); this.sender_header.show(); - this.sender_address.add(child); + this.sender_address.append(child); } // Show any Reply-To header addresses if present, but only if @@ -1088,31 +1088,36 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { address ); this.searchable_addresses.add(child); - this.reply_to_addresses.add(child); + this.reply_to_addresses.append(child); this.reply_to_header.show(); } } } } - private async void fill_header_addresses(Gtk.Grid header, + private async void fill_header_addresses(ContactList contact_list, Geary.RFC822.MailboxAddresses? addresses, GLib.Cancellable? cancellable) throws GLib.Error { - if (addresses != null && addresses.size > 0) { - ContactList box = header.get_children().nth(0).data as ContactList; - if (box != null) { - foreach (Geary.RFC822.MailboxAddress address in addresses) { - ContactFlowBoxChild child = new ContactFlowBoxChild( - yield this.contacts.load(address, cancellable), - address - ); - this.searchable_addresses.add(child); - box.add(child); - } - } - header.set_visible(true); + + // We set the visibility on the parent, as there's usually a label that + // needs to become (in)visible too. + unowned Gtk.Box header = contact_list.get_parent() as Gtk.Box; + + if (addresses == null || addresses.size <= 0) { + header.visible = false; + return; } + + foreach (Geary.RFC822.MailboxAddress address in addresses) { + ContactFlowBoxChild child = new ContactFlowBoxChild( + yield this.contacts.load(address, cancellable), + address + ); + this.searchable_addresses.add(child); + contact_list.add(child); + } + header.visible = true; } // This delegate is called from within @@ -1191,7 +1196,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { this.body_placeholder = placeholder; if (this.web_view != null) this.web_view.hide(); - this.body_container.add(placeholder); + this.body_container.append(placeholder); show_message_body(true); } else { if (this.web_view != null) @@ -1250,7 +1255,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } [GtkCallback] - private void on_address_box_child_activated(Gtk.FlowBox box, + private void on_address_box_child_activated(Gtk.Widget _unused, Gtk.FlowBoxChild child) { ContactFlowBoxChild address_child = child as ContactFlowBoxChild; if (address_child != null) { @@ -1284,10 +1289,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private bool on_context_menu(WebKit.WebView view, WebKit.ContextMenu context_menu, - Gdk.Event event, WebKit.HitTestResult hit_test) { if (this.context_menu != null) { - this.context_menu.detach(); + this.context_menu.popdown(); } // Build a new context menu every time the user clicks because @@ -1332,9 +1336,15 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { model.append_section(null, context_menu_inspector); } - this.context_menu = new Gtk.Menu.from_model(model); - this.context_menu.attach_to_widget(this, null); - this.context_menu.popup_at_pointer(event); + this.context_menu = new Gtk.PopoverMenu.from_model(model); + this.context_menu.set_parent(this); + var event = context_menu.get_event(); + double x = 0, y = 0; + if (event != null && event.get_position(out x, out y)) { + Gdk.Rectangle rect = { (int) x, (int) y, 1, 1 }; + this.context_menu.set_pointing_to(rect); + } + this.context_menu.popup(); return true; } @@ -1378,7 +1388,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { // Escape text and especially URLs since we got them from the // HREF, and Gtk.Label.set_markup is a strict parser. - var main = get_toplevel() as Application.MainWindow; + var main = get_root() as Application.MainWindow; good_link.set_markup( Markup.printf_escaped("%s", text_href, text_label) @@ -1400,7 +1410,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } ); - link_popover.set_relative_to(this.web_view); + link_popover.set_parent(this.web_view); link_popover.set_pointing_to(location); link_popover.closed.connect_after(() => { link_popover.destroy(); }); link_popover.popup(); @@ -1424,18 +1434,13 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { _("Showing remote images allows the sender to track you") ); - var menu_image = new Gtk.Image(); - menu_image.icon_name = "view-more-symbolic"; - var menu_button = new Gtk.MenuButton(); - menu_button.use_popover = true; - menu_button.image = menu_image; + menu_button.icon_name = "view-more-symbolic"; menu_button.menu_model = this.show_images_menu; menu_button.halign = Gtk.Align.END; - menu_button.hexpand =true; - menu_button.show_all(); + menu_button.hexpand = true; - this.remote_images_info_bar.get_action_area().add(menu_button); + this.remote_images_info_bar.get_action_area().append(menu_button); } else { this.remote_images_info_bar = new Components.InfoBar( // Translators: Info bar status message @@ -1456,9 +1461,15 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } private void on_copy_link(Variant? param) { - Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); - clipboard.set_text(param.get_string(), -1); - clipboard.store(); + Gdk.Clipboard clipboard = get_clipboard(); + clipboard.set_text(param.get_string()); + clipboard.store_async.begin(Priority.DEFAULT, null, (obj, res) => { + try { + clipboard.store_async.end(res); + } catch (Error err) { + debug("Couldn't copy link to clipboard: %s", err.message); + } + }); } private void on_copy_email_address(Variant? param) { @@ -1466,9 +1477,15 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { if (value.has_prefix(MAILTO_URI_PREFIX)) { value = value.substring(MAILTO_URI_PREFIX.length, -1); } - Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); - clipboard.set_text(value, -1); - clipboard.store(); + Gdk.Clipboard clipboard = get_clipboard(); + clipboard.set_text(value); + clipboard.store_async.begin(Priority.DEFAULT, null, (obj, res) => { + try { + clipboard.store_async.end(res); + } catch (Error err) { + debug("Couldn't copy email address to clipboard: %s", err.message); + } + }); } private void on_save_image(Variant? param) { @@ -1520,7 +1537,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { show_images(false); if (this.primary_contact != null) { var email_addresses = this.primary_contact.email_addresses; - foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + for (uint i = 0; i < email_addresses.get_n_items(); i++) { + var email = (Geary.RFC822.MailboxAddress) email_addresses.get_item(i); this.config.add_images_trusted_domain(email.domain); break; } @@ -1547,7 +1565,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } }); } else { - var main = this.get_toplevel() as Application.MainWindow; + var main = this.get_root() as Application.MainWindow; if (main != null) { main.application.show_uri.begin(link); } diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index 92cf113f..c58d49f3 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -10,7 +10,7 @@ * Displays the messages in a conversation and in-window composers. */ [GtkTemplate (ui = "/org/gnome/Geary/conversation-viewer.ui")] -public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { +public class ConversationViewer : Adw.Bin, Geary.BaseInterface { /** * The current conversation listbox, if any. @@ -37,14 +37,19 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { private Gee.Set? selection_while_composing = null; private GLib.Cancellable? find_cancellable = null; + [GtkChild] public unowned Components.ConversationHeaderBar headerbar; + + [GtkChild] private unowned Gtk.Stack stack; + // Stack pages - [GtkChild] private unowned Gtk.Spinner loading_page; - [GtkChild] private unowned Gtk.Grid no_conversations_page; - [GtkChild] private unowned Gtk.Grid conversation_page; - [GtkChild] private unowned Gtk.Grid multiple_conversations_page; - [GtkChild] private unowned Gtk.Grid empty_folder_page; - [GtkChild] private unowned Gtk.Grid empty_search_page; - [GtkChild] private unowned Gtk.Grid composer_page; + [GtkChild] private unowned Adw.Spinner loading_page; + [GtkChild] private unowned Adw.StatusPage no_conversations_page; + [GtkChild] private unowned Gtk.Box conversation_page; + [GtkChild] private unowned Adw.StatusPage multiple_conversations_page; + [GtkChild] private unowned Adw.StatusPage empty_folder_page; + [GtkChild] private unowned Adw.StatusPage empty_search_page; + [GtkChild] private unowned Adw.Bin composer_page; + [GtkChild] private unowned Gtk.ScrolledWindow conversation_scroller; [GtkChild] internal unowned Gtk.SearchBar conversation_find_bar; @@ -75,70 +80,6 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { base_ref(); this.config = config; - Hdy.StatusPage no_conversations = - new Hdy.StatusPage(); - no_conversations.icon_name = "folder-symbolic"; - // Translators: Title label for placeholder when no - // conversations have been selected. - no_conversations.title = _("No Conversations Selected"); - // Translators: Sub-title label for placeholder when no - // conversations have been selected. - no_conversations.description = _( - "Selecting a conversation from the list will display it here." - ); - no_conversations.hexpand = true; - no_conversations.vexpand = true; - no_conversations.show (); - this.no_conversations_page.add(no_conversations); - - Hdy.StatusPage multi_conversations = - new Hdy.StatusPage(); - multi_conversations.icon_name = "folder-symbolic"; - // Translators: Title label for placeholder when multiple - // conversations have been selected. - multi_conversations.title = _("Multiple Conversations Selected"); - // Translators: Sub-title label for placeholder when multiple - // conversations have been selected. - multi_conversations.description = _( - "Choosing an action will apply to all selected conversations." - ); - multi_conversations.hexpand = true; - multi_conversations.vexpand = true; - multi_conversations.show (); - this.multiple_conversations_page.add(multi_conversations); - - Hdy.StatusPage empty_folder = - new Hdy.StatusPage(); - empty_folder.icon_name = "folder-symbolic"; - // Translators: Title label for placeholder when no - // conversations have exist in a folder. - empty_folder.title = _("No Conversations Found"); - // Translators: Sub-title label for placeholder when no - // conversations have exist in a folder. - empty_folder.description = _( - "This folder does not contain any conversations." - ); - empty_folder.hexpand = true; - empty_folder.vexpand = true; - empty_folder.show (); - this.empty_folder_page.add(empty_folder); - - Hdy.StatusPage empty_search = - new Hdy.StatusPage(); - empty_search.icon_name = "folder-symbolic"; - // Translators: Title label for placeholder when no - // conversations have been found in a search. - empty_search.title = _("No Conversations Found"); - // Translators: Sub-title label for placeholder when no - // conversations have been found in a search. - empty_search.description = _( - "Your search returned no results, try refining your search terms." - ); - empty_search.hexpand = true; - empty_search.vexpand = true; - empty_search.show (); - this.empty_search_page.add(empty_search); - this.conversation_find_undo = new Components.EntryUndo( this.conversation_find_entry ); @@ -155,10 +96,10 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { * Puts the view into composer mode, showing a full-height composer. */ public void do_compose(Composer.Widget composer) { - var main_window = get_toplevel() as Application.MainWindow; + var main_window = get_root() as Application.MainWindow; if (main_window != null) { Composer.Box box = new Composer.Box( - composer, main_window.conversation_headerbar + composer, this.headerbar ); this.current_composer = composer; @@ -169,7 +110,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { conversation_list.unselect_all(); box.vanished.connect(on_composer_closed); - this.composer_page.add(box); + this.composer_page.child = box; set_visible_child(this.composer_page); composer.update_window_title(); } @@ -214,7 +155,6 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { * Shows the loading UI. */ public void show_loading() { - this.loading_page.start(); set_visible_child(this.loading_page); } @@ -283,13 +223,16 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { this.conversation_find_prev.set_sensitive(false); new_list.search.matches_updated.connect((count) => { bool found = count > 0; + //XXX GTK4 - Gtk.SearchEntry doesn't have a icon API anymore +#if 0 this.conversation_find_entry.set_icon_from_icon_name( Gtk.EntryIconPosition.PRIMARY, found || Geary.String.is_empty(this.conversation_find_entry.text) ? "edit-find-symbolic" : "computer-fail-symbolic" ); - this.conversation_find_next.set_sensitive(found); - this.conversation_find_prev.set_sensitive(found); +#endif + this.conversation_find_next.sensitive = found; + this.conversation_find_prev.sensitive = found; }); add_new_list(new_list); set_visible_child(this.conversation_page); @@ -318,10 +261,9 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { // are not set on the list - it makes changing focus jumpy // when a row or its web_view are larger than the viewport. Gtk.Viewport viewport = new Gtk.Viewport(null, null); - viewport.show(); - viewport.add(list); + viewport.child = list; - this.conversation_scroller.add(viewport); + this.conversation_scroller.child = viewport; } // Remove any existing conversation list, cancelling its loading @@ -329,7 +271,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { // Remove the viewport that contains the current list Gtk.Widget? scrolled_child = this.conversation_scroller.get_child(); if (scrolled_child != null) { - conversation_scroller.remove(scrolled_child); + this.conversation_scroller.child = null; } // Reset the scrollbars to their initial positions @@ -344,10 +286,14 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { return scrolled_child; } + public Gtk.Widget get_visible_child() { + return this.stack.visible_child; + } + /** * Sets the currently visible page of the stack. */ - private new void set_visible_child(Gtk.Widget widget) { + private void set_visible_child(Gtk.Widget widget) { debug("Showing: %s", widget.get_name()); Gtk.Widget current = get_visible_child(); if (current == this.conversation_page) { @@ -358,12 +304,8 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { // etc. remove_current_list(); } - } else if (current == this.loading_page) { - // Stop the spinner running so it doesn't trigger repaints - // and wake up Geary even when idle. See Bug 783025. - this.loading_page.stop(); } - base.set_visible_child(widget); + this.stack.set_visible_child(widget); } private async void update_find_results() { @@ -471,7 +413,9 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { } [GtkCallback] - private bool on_conversation_scroll() { + private bool on_conversation_scroll(Gtk.EventControllerScroll controller, + double dx, + double dy) { if (this.current_list != null) { this.current_list.mark_visible_read(); } @@ -484,7 +428,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { set_visible_child(this.conversation_page); // Restore the old selection - var main_window = get_toplevel() as Application.MainWindow; + var main_window = get_root() as Application.MainWindow; if (main_window != null) { main_window.update_title(); diff --git a/src/client/conversation-viewer/conversation-web-view.vala b/src/client/conversation-viewer/conversation-web-view.vala index f3d2c365..55adcc43 100644 --- a/src/client/conversation-viewer/conversation-web-view.vala +++ b/src/client/conversation-viewer/conversation-web-view.vala @@ -61,8 +61,8 @@ public class ConversationWebView : Components.WebView { * * A new WebKitGTK WebProcess will be constructed for this view. */ - public ConversationWebView(Application.Configuration config) { - base(config); + public ConversationWebView(Application.Configuration config, GLib.File? cache_dir) { + base(config, cache_dir); init(); // These only need to be added when creating a new WebProcess, @@ -79,6 +79,7 @@ public class ConversationWebView : Components.WebView { */ internal ConversationWebView.with_related_view( Application.Configuration config, + GLib.File? cache_dir, ConversationWebView related ) { base.with_related_view(config, related); @@ -185,38 +186,46 @@ public class ConversationWebView : Components.WebView { get_find_controller().search_finish(); } - public override bool key_press_event(Gdk.EventKey event) { + private bool on_key_pressed(Gtk.EventControllerKey controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { + //XXX GTK4 not sure what to do here // WebView consumes a number of key presses for scrolling // itself internally, but we want them to navigate around in // ConversationListBox, so don't forward any on. bool ret = Gdk.EVENT_PROPAGATE; - if (!(((int) event.keyval) in BLACKLISTED_KEY_CODES)) { - ret = base.key_press_event(event); - } + // if (!(((int) keyval) in BLACKLISTED_KEY_CODES)) { + // ret = base.key_press_event(event); + // } return ret; } - public override void get_preferred_height(out int minimum_height, - out int natural_height) { - // XXX clamp height to something not too outrageous so we - // don't get an XServer error trying to allocate a massive - // window. - const uint max_pixels = 8 * 1024 * 1024; - int width = get_allocated_width(); - int height = this.preferred_height; - if (height * width > max_pixels) { - height = (int) Math.floor(max_pixels / (double) width); + public override void measure(Gtk.Orientation orientation, + int for_size, + out int minimum, + out int natural, + out int minimum_baseline, + out int natural_baseline) { + if (orientation == Gtk.Orientation.HORIZONTAL) { + // We always want the view to be sized according to the available + // space in the parent, not by the width of the web view. + minimum = natural = 0; + } else { + // XXX clamp height to something not too outrageous so we + // don't get an XServer error trying to allocate a massive + // window. + const uint max_pixels = 8 * 1024 * 1024; + int width = get_allocated_width(); + int height = this.preferred_height; + if (height * width > max_pixels) { + height = (int) Math.floor(max_pixels / (double) width); + } + + minimum = natural = height; } - minimum_height = natural_height = height; - } - - // Overridden since we always what the view to be sized according - // to the available space in the parent, not by the width of the - // web view. - public override void get_preferred_width(out int minimum_height, - out int natural_height) { - minimum_height = natural_height = 0; + minimum_baseline = natural_baseline = -1; } private void init() { @@ -225,6 +234,10 @@ public class ConversationWebView : Components.WebView { ); this.notify["preferred-height"].connect(() => queue_resize()); + + Gtk.EventControllerKey controller = new Gtk.EventControllerKey(); + controller.key_pressed.connect(on_key_pressed); + add_controller(controller); } private void on_deceptive_link_clicked(GLib.Variant? parameters) { diff --git a/src/client/dialogs/alert-dialog.vala b/src/client/dialogs/alert-dialog.vala deleted file mode 100644 index 96abecba..00000000 --- a/src/client/dialogs/alert-dialog.vala +++ /dev/null @@ -1,128 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -class AlertDialog : Object { - private Gtk.MessageDialog dialog; - - public AlertDialog(Gtk.Window? parent, Gtk.MessageType message_type, string title, - string? description, string? ok_button, string? cancel_button, string? tertiary_button, - Gtk.ResponseType tertiary_response_type, string? ok_action_type, - string? tertiary_action_type = "", Gtk.ResponseType? default_response = null) { - - dialog = new Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, message_type, - Gtk.ButtonsType.NONE, ""); - - dialog.text = title; - dialog.secondary_text = description; - - if (!Geary.String.is_empty_or_whitespace(tertiary_button)) { - Gtk.Widget? button = dialog.add_button(tertiary_button, tertiary_response_type); - if (!Geary.String.is_empty_or_whitespace(tertiary_action_type)) { - button.get_style_context().add_class(tertiary_action_type); - } - } - - if (!Geary.String.is_empty_or_whitespace(cancel_button)) - dialog.add_button(cancel_button, Gtk.ResponseType.CANCEL); - - if (!Geary.String.is_empty_or_whitespace(ok_button)) { - Gtk.Widget? button = dialog.add_button(ok_button, Gtk.ResponseType.OK); - if (!Geary.String.is_empty_or_whitespace(ok_action_type)) { - button.get_style_context().add_class(ok_action_type); - } - } - - if (default_response != null) { - dialog.set_default_response(default_response); - } - } - - public void use_secondary_markup(bool markup) { - dialog.secondary_use_markup = markup; - } - - public Gtk.Box get_message_area() { - return (Gtk.Box) dialog.get_message_area(); - } - - public void set_focus_response(Gtk.ResponseType response) { - Gtk.Widget? to_focus = dialog.get_widget_for_response(response); - if (to_focus != null) - to_focus.grab_focus(); - } - - // Runs dialog, destroys it, and returns selected response - public Gtk.ResponseType run() { - Gtk.ResponseType response = (Gtk.ResponseType) dialog.run(); - - dialog.destroy(); - - return response; - } -} - -class ConfirmationDialog : AlertDialog { - public ConfirmationDialog(Gtk.Window? parent, string title, string? description, - string? ok_button, string? ok_action_type = "") { - base (parent, Gtk.MessageType.WARNING, title, description, ok_button, Stock._CANCEL, - null, Gtk.ResponseType.NONE, ok_action_type); - } -} - -class TernaryConfirmationDialog : AlertDialog { - public TernaryConfirmationDialog(Gtk.Window? parent, string title, string? description, - string? ok_button, string? tertiary_button, Gtk.ResponseType tertiary_response_type, - string? ok_action_type = "", string? tertiary_action_type = "", - Gtk.ResponseType? default_response = null) { - - base (parent, Gtk.MessageType.WARNING, title, description, ok_button, Stock._CANCEL, - tertiary_button, tertiary_response_type, ok_action_type, tertiary_action_type, - default_response); - } -} - -class ErrorDialog : AlertDialog { - public ErrorDialog(Gtk.Window? parent, string title, string? description) { - base (parent, Gtk.MessageType.ERROR, title, description, Stock._OK, null, null, - Gtk.ResponseType.NONE, null); - } -} - -class QuestionDialog : AlertDialog { - public bool is_checked { get; private set; default = false; } - - private Gtk.CheckButton? checkbutton = null; - - public QuestionDialog(Gtk.Window? parent, string title, string? description, - string yes_button, string no_button) { - base (parent, Gtk.MessageType.QUESTION, title, description, yes_button, no_button, null, - Gtk.ResponseType.NONE, "suggested-action"); - } - - public QuestionDialog.with_checkbox(Gtk.Window? parent, string title, string? description, - string yes_button, string no_button, string checkbox_label, bool checkbox_default) { - this (parent, title, description, yes_button, no_button); - - checkbutton = new Gtk.CheckButton.with_mnemonic(checkbox_label); - checkbutton.active = checkbox_default; - checkbutton.toggled.connect(on_checkbox_toggled); - - get_message_area().pack_start(checkbutton); - - // this must be done once all the packing is completed - get_message_area().show_all(); - - // the check box may have grabbed keyboard focus, so we put it back to the button - set_focus_response(Gtk.ResponseType.OK); - - is_checked = checkbox_default; - } - - private void on_checkbox_toggled() { - is_checked = checkbutton.active; - } -} - diff --git a/src/client/dialogs/attachment-dialog.vala b/src/client/dialogs/attachment-dialog.vala deleted file mode 100644 index 6167d34b..00000000 --- a/src/client/dialogs/attachment-dialog.vala +++ /dev/null @@ -1,104 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/** - * A FileChooser-like object for choosing attachments for a message. - */ -public class AttachmentDialog : Object { - - private const int PREVIEW_SIZE = 180; - private const int PREVIEW_PADDING = 3; - - private Application.Configuration config; - - private Gtk.FileChooserNative? chooser = null; - - private Gtk.Image preview_image = new Gtk.Image(); - - public delegate bool Attacher(File attachment_file, bool alert_errors = true); - - public AttachmentDialog(Gtk.Window? parent, Application.Configuration config) { - this.config = config; - this.chooser = new Gtk.FileChooserNative(_("Choose a file"), parent, Gtk.FileChooserAction.OPEN, _("_Attach"), Stock._CANCEL); - - this.chooser.set_local_only(false); - this.chooser.set_select_multiple(true); - - // preview widget is not supported on Win32 (this will fallback to gtk file chooser) - // and possibly by some org.freedesktop.portal.FileChooser (preview will be ignored). - this.chooser.set_preview_widget(this.preview_image); - this.chooser.use_preview_label = false; - - this.chooser.update_preview.connect(on_update_preview); - } - - public void add_filter(owned Gtk.FileFilter filter) { - this.chooser.add_filter(filter); - } - - public SList get_files() { - return this.chooser.get_files(); - } - - public int run() { - return this.chooser.run(); - } - - public void hide() { - this.chooser.hide(); - } - - public void destroy() { - this.chooser.destroy(); - } - - private void on_update_preview() { - string? filename = chooser.get_preview_filename(); - if (filename == null) { - chooser.set_preview_widget_active(false); - return; - } - - // read the image format data first - int width = 0; - int height = 0; - Gdk.PixbufFormat? format = Gdk.Pixbuf.get_file_info(filename, out width, out height); - - if (format == null) { - chooser.set_preview_widget_active(false); - return; - } - - // if the image is too big, resize it - Gdk.Pixbuf pixbuf; - try { - pixbuf = new Gdk.Pixbuf.from_file_at_scale(filename, PREVIEW_SIZE, PREVIEW_SIZE, true); - } catch (Error e) { - chooser.set_preview_widget_active(false); - return; - } - - if (pixbuf == null) { - chooser.set_preview_widget_active(false); - return; - } - - pixbuf = pixbuf.apply_embedded_orientation(); - - // distribute the extra space around the image - int extra_space = PREVIEW_SIZE - pixbuf.width; - int smaller_half = extra_space/2; - int larger_half = extra_space - smaller_half; - - // pad the image manually (avoids rounding errors) - preview_image.set_margin_start(PREVIEW_PADDING + smaller_half); - preview_image.set_margin_end(PREVIEW_PADDING + larger_half); - - // show the preview - preview_image.set_from_pixbuf(pixbuf); - chooser.set_preview_widget_active(true); - } -} diff --git a/src/client/dialogs/certificate-warning-dialog.vala b/src/client/dialogs/certificate-warning-dialog.vala index 030c3101..9101497e 100644 --- a/src/client/dialogs/certificate-warning-dialog.vala +++ b/src/client/dialogs/certificate-warning-dialog.vala @@ -4,7 +4,9 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class CertificateWarningDialog { +[GtkTemplate (ui = "/org/gnome/Geary/certificate-warning-dialog.ui")] +public class CertificateWarningDialog : Adw.AlertDialog { + public enum Result { DONT_TRUST, TRUST, @@ -13,59 +15,49 @@ public class CertificateWarningDialog { private const string BULLET = "• "; - private Gtk.Dialog dialog; + [GtkChild] private unowned Gtk.Label top_label; + [GtkChild] private unowned Gtk.Label warnings_label; + [GtkChild] private unowned Gtk.Label trust_label; + [GtkChild] private unowned Gtk.Label dont_trust_label; + [GtkChild] private unowned Gtk.Label contact_label; - public CertificateWarningDialog(Gtk.Window? parent, - Geary.AccountInformation account, + public CertificateWarningDialog(Geary.AccountInformation account, Geary.ServiceInformation service, Geary.Endpoint endpoint, bool is_validation) { - Gtk.Builder builder = GioUtil.create_builder("certificate_warning_dialog.glade"); + this.title = _("Untrusted Connection: %s").printf(account.display_name); - dialog = (Gtk.Dialog) builder.get_object("CertificateWarningDialog"); - dialog.transient_for = parent; - dialog.modal = true; - - Gtk.Label title_label = (Gtk.Label) builder.get_object("untrusted_connection_label"); - Gtk.Label top_label = (Gtk.Label) builder.get_object("top_label"); - Gtk.Label warnings_label = (Gtk.Label) builder.get_object("warnings_label"); - Gtk.Label trust_label = (Gtk.Label) builder.get_object("trust_label"); - Gtk.Label dont_trust_label = (Gtk.Label) builder.get_object("dont_trust_label"); - Gtk.Label contact_label = (Gtk.Label) builder.get_object("contact_label"); - - title_label.label = _("Untrusted Connection: %s").printf(account.display_name); - - top_label.label = _("The identity of the %s mail server at %s:%u could not be verified.").printf( + this.top_label.label = _("The identity of the %s mail server at %s:%u could not be verified.").printf( service.protocol.to_value(), service.host, service.port); - warnings_label.label = generate_warning_list( + this.warnings_label.label = generate_warning_list( endpoint.tls_validation_warnings ); - warnings_label.use_markup = true; + this.warnings_label.use_markup = true; - trust_label.label = + this.trust_label.label = "" +_("Selecting “Trust This Server” or “Always Trust This Server” may cause your username and password to be transmitted insecurely.") + ""; - trust_label.use_markup = true; + this.trust_label.use_markup = true; if (is_validation) { // could be a new or existing account - dont_trust_label.label = + this.dont_trust_label.label = "" + _("Selecting “Don’t Trust This Server” will cause Geary not to access this server.") + " " + _("Geary will not add or update this email account."); } else { // a registered account - dont_trust_label.label = + this.dont_trust_label.label = "" + _("Selecting “Don’t Trust This Server” will cause Geary to stop accessing this account.") + " "; } - dont_trust_label.use_markup = true; + this.dont_trust_label.use_markup = true; - contact_label.label = + this.contact_label.label = _("Contact your system administrator or email service provider if you have any question about these issues."); } @@ -96,17 +88,14 @@ public class CertificateWarningDialog { return builder.str; } - public Result run() { - dialog.show_all(); - int response = dialog.run(); - dialog.destroy(); + public async Result run(Gtk.Window? parent) { + string response = yield choose(parent, null); - // these values are defined in the Glade file switch (response) { - case 1: + case "trust": return Result.TRUST; - case 2: + case "always-trust": return Result.ALWAYS_TRUST; default: diff --git a/src/client/dialogs/dialogs-problem-details-dialog.vala b/src/client/dialogs/dialogs-problem-details-dialog.vala index 83943444..3a2551b9 100644 --- a/src/client/dialogs/dialogs-problem-details-dialog.vala +++ b/src/client/dialogs/dialogs-problem-details-dialog.vala @@ -9,10 +9,9 @@ * Displays technical details when a problem has been reported. */ [GtkTemplate (ui = "/org/gnome/Geary/problem-details-dialog.ui")] -public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { +public class Dialogs.ProblemDetailsDialog : Adw.Dialog { - private const string ACTION_CLOSE = "problem-details-close"; private const string ACTION_SEARCH_TOGGLE = "toggle-search"; private const string ACTION_SEARCH_ACTIVATE = "activate-search"; @@ -21,14 +20,11 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { }; private const ActionEntry[] WINDOW_ACTIONS = { - { Action.Window.CLOSE, on_close }, - { ACTION_CLOSE, on_close }, { ACTION_SEARCH_TOGGLE, on_logs_search_toggled, null, "false" }, { ACTION_SEARCH_ACTIVATE, on_logs_search_activated }, }; public static void add_accelerators(Application.Client app) { - app.add_window_accelerators(ACTION_CLOSE, { "Escape" } ); app.add_window_accelerators(ACTION_SEARCH_ACTIVATE, { "F" } ); } @@ -48,14 +44,8 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { private Geary.ServiceInformation? service; - public ProblemDetailsDialog(Gtk.Window? parent, - Application.Client application, + public ProblemDetailsDialog(Application.Client application, Geary.ProblemReport report) { - Object( - transient_for: parent, - use_header_bar: 1 - ); - Geary.AccountProblemReport? account_report = report as Geary.AccountProblemReport; Geary.ServiceProblemReport? service_report = @@ -79,9 +69,7 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { error, account, service ); - this.log_pane = new Components.InspectorLogView( - application.config, account - ); + this.log_pane = new Components.InspectorLogView(account); this.log_pane.load(report.earliest_log, report.latest_log); this.log_pane.record_selection_changed.connect( on_logs_selection_changed @@ -101,11 +89,15 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { this.stack.add_titled(this.system_pane, "system_pane", _("System")); } - public override bool key_press_event(Gdk.EventKey event) { + [GtkCallback] + private bool on_key_pressed(Gtk.EventControllerKey controller, + uint keyval, + uint keycode, + Gdk.ModifierType state) { bool ret = Gdk.EVENT_PROPAGATE; if (this.log_pane.search_mode_enabled && - event.keyval == Gdk.Key.Escape) { + keyval == Gdk.Key.Escape) { // Manually deactivate search so the button stays in sync this.search_button.set_active(false); ret = Gdk.EVENT_STOP; @@ -115,18 +107,19 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { this.log_pane.search_mode_enabled) { // Ensure and others are passed to the search // entry before getting used as an accelerator. - ret = this.log_pane.handle_key_press(event); + ret = controller.forward(this.log_pane); } - if (ret == Gdk.EVENT_PROPAGATE) { - ret = base.key_press_event(event); - } + //XXX GTK4 not sure how to handle this + // if (ret == Gdk.EVENT_PROPAGATE) { + // ret = base.key_press_event(event); + // } if (ret == Gdk.EVENT_PROPAGATE && !this.log_pane.search_mode_enabled) { // Nothing has handled the event yet, and search is not // active, so see if we want to activate it now. - ret = this.log_pane.handle_key_press(event); + ret = controller.forward(this.log_pane); if (ret == Gdk.EVENT_STOP) { this.search_button.set_active(true); } @@ -135,10 +128,9 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { return ret; } - private async void save(string path, + private async void save(GLib.File dest, GLib.Cancellable? cancellable) throws GLib.Error { - GLib.File dest = GLib.File.new_for_path(path); GLib.FileIOStream dest_io = yield dest.replace_readwrite_async( null, false, @@ -207,39 +199,29 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { string clipboard_value = (string) bytes.get_data(); if (!Geary.String.is_empty(clipboard_value)) { - get_clipboard(Gdk.SELECTION_CLIPBOARD).set_text(clipboard_value, -1); + get_clipboard().set_text(clipboard_value); } } [GtkCallback] private void on_save_as_clicked() { - Gtk.FileChooserNative chooser = new Gtk.FileChooserNative( - _("Save As"), - this, - Gtk.FileChooserAction.SAVE, - _("Save As"), - _("Cancel") - ); - chooser.set_current_name( - new GLib.DateTime.now_local().format( - "Geary Problem Report - %F %T.txt" - ) + save_as.begin(); + } + + private async void save_as() { + var dialog = new Gtk.FileDialog(); + dialog.title = _("Save As"); + dialog.accept_label = _("Save As"); + dialog.initial_name = new DateTime.now_local().format( + "Geary Problem Report - %F %T.txt" ); - if (chooser.run() == Gtk.ResponseType.ACCEPT) { - this.save.begin( - chooser.get_filename(), - null, - (obj, res) => { - try { - this.save.end(res); - } catch (GLib.Error err) { - warning( - "Failed to save problem report data: %s", err.message - ); - } - } - ); + try { + File? file = yield dialog.save(get_root() as Gtk.Window, null); + if (file != null) + yield this.save(file, null); + } catch (Error err) { + warning("Failed to save problem report data: %s", err.message); } } @@ -257,9 +239,4 @@ public class Dialogs.ProblemDetailsDialog : Gtk.Dialog { private void on_logs_search_activated() { this.search_button.set_active(true); } - - private void on_close() { - destroy(); - } - } diff --git a/src/client/dialogs/password-dialog.vala b/src/client/dialogs/password-dialog.vala index d566c040..a517e211 100644 --- a/src/client/dialogs/password-dialog.vala +++ b/src/client/dialogs/password-dialog.vala @@ -8,74 +8,50 @@ * Displays a dialog for collecting the user's password, without allowing them to change their * other data. */ -public class PasswordDialog { - // We can't keep these in the glade file, because Gnome doesn't want markup in translatable - // strings, and Glade doesn't support the "larger" size attribute. See this bug report for - // details: https://bugzilla.gnome.org/show_bug.cgi?id=679006 - private const string PRIMARY_TEXT_MARKUP = "%s"; - private const string PRIMARY_TEXT_FIRST_TRY = _("Geary requires your email password to continue"); +[GtkTemplate (ui = "/org/gnome/Geary/password-dialog.ui")] +public class PasswordDialog : Adw.AlertDialog { - private Gtk.Dialog dialog; - private Gtk.Entry entry_password; - private Gtk.CheckButton check_remember_password; - private Gtk.Button ok_button; - - public string password { get; private set; default = ""; } - public bool remember_password { get; private set; } + [GtkChild] private unowned Adw.PreferencesGroup prefs_group; + [GtkChild] private unowned Adw.ActionRow username_row; + [GtkChild] private unowned Adw.PasswordEntryRow password_row; + [GtkChild] private unowned Adw.SwitchRow remember_password_row; public PasswordDialog(Gtk.Window? parent, Geary.AccountInformation account, Geary.ServiceInformation service, Geary.Credentials? credentials) { - Gtk.Builder builder = GioUtil.create_builder("password-dialog.glade"); - - dialog = (Gtk.Dialog) builder.get_object("PasswordDialog"); - dialog.transient_for = parent; - dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG); - dialog.set_default_response(Gtk.ResponseType.OK); - - entry_password = (Gtk.Entry) builder.get_object("entry: password"); - check_remember_password = (Gtk.CheckButton) builder.get_object("check: remember_password"); - - Gtk.Label label_username = (Gtk.Label) builder.get_object("label: username"); - Gtk.Label label_smtp = (Gtk.Label) builder.get_object("label: smtp"); - - // Load translated text for labels with markup unsupported by glade. - Gtk.Label primary_text_label = (Gtk.Label) builder.get_object("primary_text_label"); - primary_text_label.set_markup(PRIMARY_TEXT_MARKUP.printf(PRIMARY_TEXT_FIRST_TRY)); - if (credentials != null) { - label_username.set_text(credentials.user); - entry_password.set_text(credentials.token ?? ""); + this.username_row.subtitle = credentials.user; + this.password_row.text = credentials.token ?? ""; } - check_remember_password.active = service.remember_password; + this.remember_password_row.active = service.remember_password; if ((service.protocol == Geary.Protocol.SMTP)) { - label_smtp.show(); + this.prefs_group.title = _("SMTP Credentials"); } - ok_button = (Gtk.Button) builder.get_object("authenticate_button"); - refresh_ok_button_sensitivity(); - entry_password.changed.connect(refresh_ok_button_sensitivity); + this.password_row.changed.connect(refresh_ok_button_sensitivity); } private void refresh_ok_button_sensitivity() { - ok_button.sensitive = !Geary.String.is_empty_or_whitespace(entry_password.get_text()); + string password = this.password_row.text; + set_response_enabled("authenticate", !Geary.String.is_empty_or_whitespace(password)); } - public bool run() { - dialog.show(); + public async string? get_password(Gtk.Window? parent, + out bool remember_password) { + string response = yield choose(parent, null); - Gtk.ResponseType response = (Gtk.ResponseType) dialog.run(); - if (response == Gtk.ResponseType.OK) { - password = entry_password.get_text(); - remember_password = check_remember_password.active; + if (response == "cancel") { + remember_password = false; + return null; } - dialog.destroy(); - - return (response == Gtk.ResponseType.OK); + remember_password = this.remember_password_row.active; + string password = this.password_row.text; + close(); + return password; } } diff --git a/src/client/folder-list/folder-list-folder-entry.vala b/src/client/folder-list/folder-list-folder-entry.vala index 1a00ca3c..750fcf01 100644 --- a/src/client/folder-list/folder-list-folder-entry.vala +++ b/src/client/folder-list/folder-list-folder-entry.vala @@ -80,6 +80,8 @@ public class FolderList.FolderEntry : entry_changed(); } + //XXX GTK4 I have no idea yet +#if 0 public bool internal_drop_received(Sidebar.Tree parent, Gdk.DragContext context, Gtk.SelectionData data) { @@ -104,6 +106,7 @@ public class FolderList.FolderEntry : } return handled; } +#endif public override int get_count() { switch (this.context.displayed_count) { diff --git a/src/client/folder-list/folder-list-tree.vala b/src/client/folder-list/folder-list-tree.vala index 732d6cd8..52a5c567 100644 --- a/src/client/folder-list/folder-list-tree.vala +++ b/src/client/folder-list/folder-list-tree.vala @@ -7,10 +7,6 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { - public const Gtk.TargetEntry[] TARGET_ENTRY_LIST = { - { "application/x-geary-mail", Gtk.TargetFlags.SAME_APP, 0 } - }; - private const int INBOX_ORDINAL = -2; // First account branch is zero private const int SEARCH_ORDINAL = -1; @@ -29,7 +25,9 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { public Tree() { - base(TARGET_ENTRY_LIST, Gdk.DragAction.COPY | Gdk.DragAction.MOVE, drop_handler); + //XXX GTK4 need to set up proper GdkContentFormats here + base(new Gdk.ContentFormats({ "application/x-geary-mail" }), + Gdk.DragAction.COPY | Gdk.DragAction.MOVE); base_ref(); set_activate_on_single_click(true); entry_selected.connect(on_entry_selected); @@ -37,9 +35,12 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { // GtkTreeView binds Ctrl+N to "move cursor to next". Not so interested in that, so we'll // remove it. + //XXX GTK4 +#if 0 unowned Gtk.BindingSet? binding_set = Gtk.BindingSet.find("GtkTreeView"); assert(binding_set != null); Gtk.BindingEntry.remove(binding_set, Gdk.Key.N, Gdk.ModifierType.CONTROL_MASK); +#endif this.visible = true; } @@ -48,9 +49,20 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { base_unref(); } - public override void get_preferred_width(out int minimum_size, out int natural_size) { - minimum_size = 360; - natural_size = 500; + public override void measure(Gtk.Orientation orientation, + int for_size, + out int minimum, + out int natural, + out int minimum_baseline, + out int natural_baseline) { + if (orientation == Gtk.Orientation.HORIZONTAL) { + minimum = 180; + natural = 300; + } else { + //XXX GTK4 - I have no idea what to put here + } + + minimum_baseline = natural_baseline = -1; } public void set_has_new(Geary.Folder folder, bool has_new) { @@ -68,10 +80,6 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { } } - private void drop_handler(Gdk.DragContext context, Sidebar.Entry? entry, - Gtk.SelectionData data, uint info, uint time) { - } - private FolderEntry? get_folder_entry(Geary.Folder folder) { AccountBranch? account_branch = account_branches.get(folder.account); return (account_branch == null ? null : @@ -80,7 +88,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { public override bool accept_cursor_changed() { bool can_switch = true; - var parent = get_toplevel() as Application.MainWindow; + var parent = get_root() as Application.MainWindow; if (parent != null) { can_switch = parent.close_composer(false); } @@ -221,6 +229,8 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { folder_selected(null); } + // XXX GTK4 I'm not sur eif this is needed still? +#if 0 public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) { // Run the base version first. bool ret = base.drag_motion(context, x, y, time); @@ -236,6 +246,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface { } return ret; } +#endif public void set_search(Geary.Engine engine, Geary.App.SearchFolder search_folder) { diff --git a/src/client/meson.build b/src/client/meson.build index 187e0e4e..744ca0c9 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -41,7 +41,10 @@ client_vala_sources = files( 'accounts/accounts-editor-list-pane.vala', 'accounts/accounts-editor-row.vala', 'accounts/accounts-editor-servers-pane.vala', + 'accounts/accounts-mailbox-editor-dialog.vala', + 'accounts/accounts-service-information-widget.vala', 'accounts/accounts-signature-web-view.vala', + 'accounts/accounts-tls-combo-row.vala', 'accounts/accounts-manager.vala', 'client-action.vala', @@ -49,31 +52,29 @@ client_vala_sources = files( 'components/components-attachment-pane.vala', 'components/components-conversation-actions.vala', 'components/components-entry-undo.vala', - 'components/components-headerbar-application.vala', - 'components/components-headerbar-conversation-list.vala', 'components/components-headerbar-conversation.vala', 'components/components-info-bar-stack.vala', 'components/components-info-bar.vala', 'components/components-inspector.vala', - 'components/components-in-app-notification.vala', 'components/components-inspector-error-view.vala', 'components/components-inspector-log-view.vala', 'components/components-inspector-system-view.vala', 'components/components-placeholder-pane.vala', - 'components/components-preferences-window.vala', + 'components/components-preferences-dialog.vala', 'components/components-problem-report-info-bar.vala', 'components/components-reflow-box.c', 'components/components-search-bar.vala', 'components/components-validator.vala', + 'components/components-validator-group.vala', 'components/components-web-view.vala', 'components/count-badge.vala', 'components/folder-popover.vala', 'components/folder-popover-row.vala', - 'components/icon-factory.vala', 'components/monitored-progress-bar.vala', 'components/monitored-spinner.vala', 'components/stock.vala', + 'composer/composer-addresses-row.vala', 'composer/composer-application-interface.vala', 'composer/composer-box.vala', 'composer/composer-container.vala', @@ -100,8 +101,6 @@ client_vala_sources = files( 'conversation-viewer/conversation-viewer.vala', 'conversation-viewer/conversation-web-view.vala', - 'dialogs/alert-dialog.vala', - 'dialogs/attachment-dialog.vala', 'dialogs/certificate-warning-dialog.vala', 'dialogs/dialogs-problem-details-dialog.vala', 'dialogs/password-dialog.vala', @@ -162,18 +161,18 @@ client_dependencies = [ gio, gmime, goa, - gspell, gtk, icu_uc, javascriptcoregtk, json_glib, - libhandy, + libadwaita, libmath, libpeas, libsecret, + libspelling, libxml, posix, - webkit2gtk, + webkitgtk, ] client_build_dir = meson.current_build_dir() @@ -191,7 +190,7 @@ client_vala_args += [ ) ] -if webkit2gtk.version().version_compare('<2.31') +if webkitgtk.version().version_compare('<2.31') client_vala_args += [ '--define=WEBKIT_PLUGINS_SUPPORTED' ] endif diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala b/src/client/plugin/desktop-notifications/desktop-notifications.vala index 6e7cee49..e3debcfb 100644 --- a/src/client/plugin/desktop-notifications/desktop-notifications.vala +++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala @@ -97,7 +97,7 @@ public class Plugin.DesktopNotifications : Email email ) throws GLib.Error { string title = to_notitication_title(folder.account, total); - GLib.Icon icon = null; + GLib.Icon? icon = null; Geary.RFC822.MailboxAddress? originator = email.get_primary_originator(); if (originator != null) { ContactStore contacts = @@ -132,20 +132,44 @@ public class Plugin.DesktopNotifications : ); } - int window_scale = 1; - Gdk.Display? display = Gdk.Display.get_default(); - if (display != null) { - Gdk.Monitor? monitor = display.get_primary_monitor(); - if (monitor != null) { - window_scale = monitor.scale_factor; - } + Gdk.Texture texture; + if (icon != null) { + var icon_stream = yield ((LoadableIcon) icon).load_async(32, null); + var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(icon_stream, 32, 32, false); + texture = Gdk.Texture.for_pixbuf(pixbuf); + } else { + texture = generate_fallback_avatar(title); } - var avatar = new Hdy.Avatar(32, title, true); - avatar.loadable_icon = icon as GLib.LoadableIcon; - icon = yield avatar.draw_to_pixbuf_async(32, window_scale, null); + issue_arrived_notification(title, body, texture, folder, email.identifier); + } - issue_arrived_notification(title, body, icon, folder, email.identifier); + private Gdk.Texture generate_fallback_avatar(string title) { + Gsk.Renderer renderer = new Gsk.VulkanRenderer(); + try { + renderer.realize(null); + } catch (GLib.Error error) { + warning("Couldn't realize vulkan renderer: %s", error.message); + renderer = new Gsk.CairoRenderer(); + try { + renderer.realize(null); + } catch (GLib.Error error) { + warning("Couldn't realize Cairo renderer: %s", error.message); + } + } + + var avatar = new Adw.Avatar(32, title, true); + var paintable = new Gtk.WidgetPaintable(avatar); + + // Ideally we could use Adw.Avatar.draw_to_texture(), + // but that unfortunately relies on a Gtk.Native existing already + var snapshot = new Gtk.Snapshot(); + paintable.snapshot(snapshot, 32, 32); + Gsk.RenderNode node = snapshot.to_node(); + Gdk.Texture texture = renderer.render_texture(node, null); + + renderer.unrealize(); + return texture; } private void notify_general(Folder folder, int total, int added) { diff --git a/src/client/plugin/mail-merge/mail-merge.vala b/src/client/plugin/mail-merge/mail-merge.vala index 91d4b880..90bdca27 100644 --- a/src/client/plugin/mail-merge/mail-merge.vala +++ b/src/client/plugin/mail-merge/mail-merge.vala @@ -178,7 +178,7 @@ public class Plugin.MailMerge : private async void merge_email(EmailIdentifier id, GLib.File? default_csv_file) { - var csv_file = default_csv_file ?? show_merge_data_chooser(); + var csv_file = default_csv_file ?? yield show_merge_data_chooser(); if (csv_file != null) { try { var csv_input = yield csv_file.read_async( @@ -297,7 +297,7 @@ public class Plugin.MailMerge : } private async void load_composer_data(Composer composer) { - var data = show_merge_data_chooser(); + var data = yield show_merge_data_chooser(); if (data != null) { var insert_field_action = new GLib.SimpleAction( ACTION_INSERT_FIELD, @@ -388,26 +388,20 @@ public class Plugin.MailMerge : return action_bar; } - private GLib.File? show_merge_data_chooser() { - var chooser = new Gtk.FileChooserNative( - /// Translators: File chooser title after invoking mail - /// merge in composer - _("Mail Merge"), - null, OPEN, - _("_Open"), - _("_Cancel") - ); + private async GLib.File? show_merge_data_chooser() { + var dialog = new Gtk.FileDialog(); + /// Translators: Filechooser title after invoking mail merge in composer + dialog.title = _("Mail Merge"); + var csv_filter = new Gtk.FileFilter(); /// Translators: File chooser filer label csv_filter.set_filter_name(_("Comma separated values (CSV)")); csv_filter.add_mime_type("text/csv"); - chooser.add_filter(csv_filter); + var filters = new GLib.ListStore(typeof(Gtk.FileFilter)); + filters.append(csv_filter); + dialog.filters = filters; - return ( - chooser.run() == Gtk.ResponseType.ACCEPT - ? chooser.get_file() - : null - ); + return yield dialog.open(null, null); } private void insert_field(Composer composer, string field) { diff --git a/src/client/plugin/mail-merge/meson.build b/src/client/plugin/mail-merge/meson.build index 7e440422..3fc0d728 100644 --- a/src/client/plugin/mail-merge/meson.build +++ b/src/client/plugin/mail-merge/meson.build @@ -59,4 +59,4 @@ plugin_test = executable( install: false ) -test(plugin_name + '-test', plugin_test) +# test(plugin_name + '-test', plugin_test) diff --git a/src/client/plugin/meson.build b/src/client/plugin/meson.build index 024e6506..7f9f8a82 100644 --- a/src/client/plugin/meson.build +++ b/src/client/plugin/meson.build @@ -6,7 +6,6 @@ plugin_dependencies = [ config_dep, folks, - gdk, client_dep, engine_dep, gee, @@ -14,10 +13,10 @@ plugin_dependencies = [ goa, gtk, javascriptcoregtk, - libhandy, + libadwaita, libmath, libpeas, - webkit2gtk, + webkitgtk, ] plugin_c_args = geary_c_args diff --git a/src/client/sidebar/sidebar-common.vala b/src/client/sidebar/sidebar-common.vala index d4c702c7..4f767b6b 100644 --- a/src/client/sidebar/sidebar-common.vala +++ b/src/client/sidebar/sidebar-common.vala @@ -85,6 +85,7 @@ public class Sidebar.Header : Sidebar.Grouping, Sidebar.EmphasizableEntry { public interface Sidebar.Contextable : Object { // Return null if the context menu should not be invoked for this event - public abstract Gtk.Menu? get_sidebar_context_menu(Gdk.EventButton event); + //XXX GTK4 is this used? + // public abstract Gtk.PopoverMenu? get_sidebar_context_menu(Gdk.EventButton event); } diff --git a/src/client/sidebar/sidebar-count-cell-renderer.vala b/src/client/sidebar/sidebar-count-cell-renderer.vala index c6bd1bfb..9100d491 100644 --- a/src/client/sidebar/sidebar-count-cell-renderer.vala +++ b/src/client/sidebar/sidebar-count-cell-renderer.vala @@ -27,24 +27,19 @@ public class SidebarCountCellRenderer : Gtk.CellRenderer { natural_size = minimum_size; } - public override void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area, - Gdk.Rectangle cell_area, Gtk.CellRendererState flags) { - unread_count.count = counter; + public override void snapshot(Gtk.Snapshot snapshot, + Gtk.Widget widget, + Gdk.Rectangle background_area, + Gdk.Rectangle cell_area, + Gtk.CellRendererState flags) { + this.unread_count.count = this.counter; + Graphene.Rect cell_rect = { { cell_area.x, cell_area.y } , { cell_area.width, cell_area.height } }; + Cairo.Context ctx = snapshot.append_cairo(cell_rect); // Compute x and y locations to right-align and vertically center the count. int x = cell_area.x + (cell_area.width - unread_count.get_width(widget)) - HORIZONTAL_MARGIN; int y = cell_area.y + ((cell_area.height - unread_count.get_height(widget)) / 2); unread_count.render(widget, ctx, x, y, false); } - - // This is implemented because it's required; ignore it and look at get_preferred_width() instead. - public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset, - out int y_offset, out int width, out int height) { - // Set values to avoid compiler warning. - x_offset = 0; - y_offset = 0; - width = 0; - height = 0; - } } diff --git a/src/client/sidebar/sidebar-entry.vala b/src/client/sidebar/sidebar-entry.vala index 750d5a7f..63c804f8 100644 --- a/src/client/sidebar/sidebar-entry.vala +++ b/src/client/sidebar/sidebar-entry.vala @@ -50,12 +50,16 @@ public interface Sidebar.DestroyableEntry : Sidebar.Entry { } public interface Sidebar.InternalDropTargetEntry : Sidebar.Entry { + //XXX GTK4 I have no idea yet +#if 0 // Returns true if drop was successful public abstract bool internal_drop_received(Sidebar.Tree parent, Gdk.DragContext context, Gtk.SelectionData data); +#endif } public interface Sidebar.InternalDragSourceEntry : Sidebar.Entry { - public abstract void prepare_selection_data(Gtk.SelectionData data); + //XXX GTK4: is this even used? + // public abstract void prepare_selection_data(Gtk.SelectionData data); } diff --git a/src/client/sidebar/sidebar-tree.vala b/src/client/sidebar/sidebar-tree.vala index f90f1c25..db42f7c8 100644 --- a/src/client/sidebar/sidebar-tree.vala +++ b/src/client/sidebar/sidebar-tree.vala @@ -9,8 +9,7 @@ public class Sidebar.Tree : Gtk.TreeView { // Only one ExternalDropHandler can be registered with the Tree; it's responsible for completing // the "drag-data-received" signal properly. - public delegate void ExternalDropHandler(Gdk.DragContext context, Sidebar.Entry? entry, - Gtk.SelectionData data, uint info, uint time); + public delegate void ExternalDropHandler(Gtk.DropTarget context, Sidebar.Entry? entry); private class EntryWrapper : Object { public Sidebar.Entry entry; @@ -72,7 +71,7 @@ public class Sidebar.Tree : Gtk.TreeView { private int editing_disabled = 0; private bool mask_entry_selected_signal = false; private weak EntryWrapper? selected_wrapper = null; - private Gtk.Menu? default_context_menu = null; + private Gtk.PopoverMenu? default_context_menu = null; private bool is_internal_drag_in_progress = false; private Sidebar.Entry? internal_drag_source_entry = null; private Gtk.TreeRowReference? old_path_ref = null; @@ -89,11 +88,10 @@ public class Sidebar.Tree : Gtk.TreeView { public signal void branch_shown(Sidebar.Branch branch, bool shown); - public Tree(Gtk.TargetEntry[] target_entries, Gdk.DragAction actions, - ExternalDropHandler drop_handler, Gtk.IconTheme? theme = null) { + public Tree(Gdk.ContentFormats formats, Gdk.DragAction actions, Gtk.IconTheme? theme = null) { set_model(store); icon_theme = theme; - get_style_context().add_class("sidebar"); + add_css_class("navigation-sidebar"); text_column = new Gtk.TreeViewColumn(); text_column.set_expand(true); @@ -131,7 +129,7 @@ public class Sidebar.Tree : Gtk.TreeView { // It Would Be Nice if the target entries and actions were gleaned by querying each // Sidebar.Entry as it was added, but that's a tad too complicated for our needs // currently - enable_model_drag_dest(target_entries, actions); + enable_model_drag_dest(formats, actions); // Drag source removed as per http://redmine.yorba.org/issues/4701 // @@ -143,11 +141,23 @@ public class Sidebar.Tree : Gtk.TreeView { this.drop_handler = drop_handler; - popup_menu.connect(on_context_menu_keypress); + Gtk.DragSource drag_source = new Gtk.DragSource(); + drag_source.drag_begin.connect(on_drag_source_begin); + drag_source.drag_end.connect(on_drag_source_end); + drag_source.prepare.connect(on_drag_source_prepare); + add_controller(drag_source); - drag_begin.connect(on_drag_begin); - drag_end.connect(on_drag_end); - drag_motion.connect(on_drag_motion); + //XXX GTK4 - need to figure out the params still + Gtk.DropTarget drop_target = new Gtk.DropTarget(Type.INVALID, Gdk.DragAction.COPY | Gdk.DragAction.MOVE); + drop_target.enter.connect(on_drop_target_enter); + add_controller(drop_target); + + var key_controller = new Gtk.EventControllerKey(); + key_controller.key_pressed.connect(on_key_pressed); + add_controller(key_controller); + var click_gesture = new Gtk.GestureClick(); + click_gesture.pressed.connect(on_button_pressed); + add_controller(click_gesture); } ~Tree() { @@ -172,29 +182,31 @@ public class Sidebar.Tree : Gtk.TreeView { renderer.visible = counter_renderer != null && counter_renderer.counter > 0; } - private void on_drag_begin(Gdk.DragContext ctx) { - is_internal_drag_in_progress = true; + private void on_drag_source_begin(Gtk.DragSource drag_source, Gdk.Drag drag) { + this.is_internal_drag_in_progress = true; } - private void on_drag_end(Gdk.DragContext ctx) { - is_internal_drag_in_progress = false; - internal_drag_source_entry = null; + private void on_drag_source_end(Gtk.DragSource drag_source, Gdk.Drag drag, bool delete_data) { + this.is_internal_drag_in_progress = false; + this.internal_drag_source_entry = null; } - private bool on_drag_motion (Gdk.DragContext context, int x, int y, uint time_) { - if (is_internal_drag_in_progress && internal_drag_source_entry == null) { + private Gdk.DragAction on_drop_target_enter(Gtk.DropTarget drop_target, double x, double y) { + if (this.is_internal_drag_in_progress && this.internal_drag_source_entry == null) { Gtk.TreePath? path; Gtk.TreeViewDropPosition position; - get_dest_row_at_pos(x, y, out path, out position); + get_dest_row_at_pos((int) x, (int) y, out path, out position); if (path != null) { EntryWrapper wrapper = get_wrapper_at_path(path); - if (wrapper != null) - internal_drag_source_entry = wrapper.entry; + if (wrapper != null) { + this.internal_drag_source_entry = wrapper.entry; + return Gdk.DragAction.COPY | Gdk.DragAction.MOVE; + } } } - return false; + return 0; } private bool has_wrapper(Sidebar.Entry entry) { @@ -231,8 +243,8 @@ public class Sidebar.Tree : Gtk.TreeView { return get_wrapper_at_iter(iter); } - public void set_default_context_menu(Gtk.Menu context_menu) { - default_context_menu = context_menu; + public void set_default_context_menu(Gtk.PopoverMenu context_menu) { + this.default_context_menu = context_menu; } // Note that this method will result in the "entry-selected" signal to fire if mask_signal @@ -296,7 +308,7 @@ public class Sidebar.Tree : Gtk.TreeView { return true; } - public override void row_activated(Gtk.TreePath path, Gtk.TreeViewColumn column) { + public override void row_activated(Gtk.TreePath path, Gtk.TreeViewColumn? column) { if (column != text_column) return; @@ -750,17 +762,10 @@ public class Sidebar.Tree : Gtk.TreeView { return (wrapper != null) ? (wrapper.entry is Sidebar.SelectableEntry) : false; } - private Gtk.TreePath? get_path_from_event(Gdk.EventButton event) { - int x, y; - Gdk.ModifierType mask; - event.window.get_device_position( - event.get_seat().get_pointer(), - out x, out y, out mask - ); - + private Gtk.TreePath? get_path_from_position(double x, double y) { int cell_x, cell_y; Gtk.TreePath path; - return get_path_at_pos(x, y, out path, null, out cell_x, out cell_y) ? path : null; + return get_path_at_pos((int) x, (int) y, out path, null, out cell_x, out cell_y) ? path : null; } private Gtk.TreePath? get_current_path() { @@ -771,63 +776,57 @@ public class Sidebar.Tree : Gtk.TreeView { return rows.length() != 0 ? rows.nth_data(0) : null; } - private bool on_context_menu_keypress() { - GLib.List rows = get_selection().get_selected_rows(null); - if (rows == null) - return false; - - Gtk.TreePath? path = rows.data; - if (path == null) - return false; - - scroll_to_cell(path, null, false, 0, 0); - - return popup_context_menu(path); - } - - private bool popup_context_menu(Gtk.TreePath path, Gdk.EventButton? event = null) { + private bool popup_context_menu(Gtk.TreePath path, Gdk.Rectangle? area = null) { EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) return false; + //XXX GTK4 +#if 0 Sidebar.Contextable? contextable = wrapper.entry as Sidebar.Contextable; if (contextable == null) return false; - Gtk.Menu? context_menu = contextable.get_sidebar_context_menu(event); + Gtk.PopoverMenu? context_menu = contextable.get_sidebar_context_menu(event); if (context_menu == null) return false; - context_menu.popup_at_pointer(event); + if (area != null) + context_menu.set_pointing_to(area); + context_menu.popup(); +#endif return true; } - private bool popup_default_context_menu(Gdk.EventButton event) { - if (default_context_menu != null) - default_context_menu.popup_at_pointer(event); - return true; + private void popup_default_context_menu(Gdk.Rectangle area) { + if (this.default_context_menu == null) + return; + this.default_context_menu.set_pointing_to(area); + this.default_context_menu.popup(); } - public override bool button_press_event(Gdk.EventButton event) { - Gtk.TreePath? path = get_path_from_event(event); + private void on_button_pressed(Gtk.GestureClick click_gesture, int n_pressed, double x, double y) { + Gtk.TreePath? path = get_path_from_position(x, y); - if (event.button == 3 && event.type == Gdk.EventType.BUTTON_PRESS) { + var button = click_gesture.get_current_button(); + if (button == Gdk.BUTTON_SECONDARY && n_pressed == 1) { + Gdk.Rectangle rect = { (int) x, (int) y, 1, 1 }; // single right click if (path != null) - popup_context_menu(path, event); + popup_context_menu(path, rect); else - popup_default_context_menu(event); - } else if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS) { + popup_default_context_menu(rect); + } else if (button == Gdk.BUTTON_PRIMARY) { if (path == null) { old_path_ref = null; - return base.button_press_event(event); + return; } EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) { old_path_ref = null; - return base.button_press_event(event); + return; } // Is this a click on an already-highlighted tree item? @@ -836,7 +835,7 @@ public class Sidebar.Tree : Gtk.TreeView { // yes, don't allow single-click editing, but // pass the event on for dragging. text_renderer.editable = false; - return base.button_press_event(event); + return; } // Got click on different tree item, make sure it is editable @@ -849,13 +848,11 @@ public class Sidebar.Tree : Gtk.TreeView { // Remember what tree item is highlighted for next time. old_path_ref = new Gtk.TreeRowReference(store, path); } - - return base.button_press_event(event); } - public override bool key_press_event(Gdk.EventKey event) { + private bool on_key_pressed(Gtk.EventControllerKey key_controller, uint keyval, uint keycode, Gdk.ModifierType state) { bool handled = false; - switch (Gdk.keyval_name(event.keyval)) { + switch (Gdk.keyval_name(keyval)) { case "F2": handled = rename_in_place(); break; @@ -865,9 +862,6 @@ public class Sidebar.Tree : Gtk.TreeView { handled = (path != null) ? destroy_path(path) : false; break; } - if (!handled) { - handled = base.key_press_event(event); - } return handled; } @@ -905,35 +899,40 @@ public class Sidebar.Tree : Gtk.TreeView { return true; } - public override void drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data, - uint info, uint time) { - InternalDragSourceEntry? drag_source = null; + private Gdk.ContentProvider? on_drag_source_prepare(Gtk.DragSource drag_source, + double x, + double y) { + InternalDragSourceEntry? drag_source_entry = null; if (internal_drag_source_entry != null) { Sidebar.SelectableEntry selectable = internal_drag_source_entry as Sidebar.SelectableEntry; if (selectable == null) { - drag_source = internal_drag_source_entry as InternalDragSourceEntry; + drag_source_entry = internal_drag_source_entry as InternalDragSourceEntry; } } - if (drag_source == null) { + if (drag_source_entry == null) { Gtk.TreePath? selected_path = get_selected_path(); if (selected_path == null) - return; + return null; EntryWrapper? wrapper = get_wrapper_at_path(selected_path); if (wrapper == null) - return; + return null; - drag_source = wrapper.entry as InternalDragSourceEntry; - if (drag_source == null) - return; + drag_source_entry = wrapper.entry as InternalDragSourceEntry; + if (drag_source_entry == null) + return null; } - drag_source.prepare_selection_data(selection_data); + //XXX GTK4, it looks like nothing is implementing this? + // drag_source_entry.prepare_selection_data(selection_data); + return null; //XXX GTK4 what do I return here? } + //XXX GTK4 not sure how to do this yet +#if 0 public override void drag_data_received(Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { @@ -974,10 +973,13 @@ public class Sidebar.Tree : Gtk.TreeView { return; } + //XXX GTK4 I have no idea yet +#if 0 bool success = targetable.internal_drop_received( this, context, selection_data ); Gtk.drag_finish(context, success, false, time); +#endif } public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) { @@ -998,6 +1000,7 @@ public class Sidebar.Tree : Gtk.TreeView { return has_dest; } +#endif // Returns true if path is renameable, and selects the path as well. private bool can_rename_path(Gtk.TreePath path) { @@ -1038,7 +1041,7 @@ public class Sidebar.Tree : Gtk.TreeView { if (editable is Gtk.Entry) { text_entry = (Gtk.Entry) editable; text_entry.editing_done.connect(on_editing_done); - text_entry.focus_out_event.connect(on_editing_focus_out); + // text_entry.focus_out_event.connect(on_editing_focus_out); text_entry.editable = true; } } @@ -1047,7 +1050,7 @@ public class Sidebar.Tree : Gtk.TreeView { text_entry.editable = false; text_entry.editing_done.disconnect(on_editing_done); - text_entry.focus_out_event.disconnect(on_editing_focus_out); + // text_entry.focus_out_event.disconnect(on_editing_focus_out); } private void on_editing_done() { @@ -1061,14 +1064,17 @@ public class Sidebar.Tree : Gtk.TreeView { } text_entry.editing_done.disconnect(on_editing_done); - text_entry.focus_out_event.disconnect(on_editing_focus_out); + // text_entry.focus_out_event.disconnect(on_editing_focus_out); } + //XXX GTK4 I(m not sure how to remove the focus controller again, so commenting out for now +#if 0 private bool on_editing_focus_out(Gdk.EventFocus event) { // We'll return false here, in case other parts of the app // want to know if the button press event that caused // us to lose focus have been fully handled. return false; } +#endif } diff --git a/src/client/util/util-contact.vala b/src/client/util/util-contact.vala index 284189fc..04d22572 100644 --- a/src/client/util/util-contact.vala +++ b/src/client/util/util-contact.vala @@ -23,7 +23,8 @@ namespace Util.Contact { return true; // Contact domain trusted } else { - foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + for (uint i = 0; i < email_addresses.get_n_items(); i++) { + var email = (Geary.RFC822.MailboxAddress) email_addresses.get_item(i); if (email.domain in domains) { return true; } diff --git a/src/client/util/util-gtk.vala b/src/client/util/util-gtk.vala index 63e27e13..c098d8ed 100644 --- a/src/client/util/util-gtk.vala +++ b/src/client/util/util-gtk.vala @@ -85,8 +85,7 @@ namespace Util.Gtk { */ public inline int get_border_box_height(global::Gtk.Widget widget) { global::Gtk.StyleContext style = widget.get_style_context(); - global::Gtk.StateFlags flags = style.get_state(); - global::Gtk.Border margin = style.get_margin(flags); + global::Gtk.Border margin = style.get_margin(); return widget.get_allocated_height() - margin.top - margin.bottom; } @@ -218,15 +217,6 @@ namespace Util.Gtk { return new_url; } - public Gdk.RGBA rgba(double red, double green, double blue, double alpha) { - return Gdk.RGBA() { - red = red, - green = green, - blue = blue, - alpha = alpha - }; - } - /* Connect this to Gtk.Widget.query_tooltip signal, will only show tooltip if label ellipsized */ public bool query_tooltip_label(global::Gtk.Widget widget, int x, int y, bool keyboard, global::Gtk.Tooltip tooltip) { global::Gtk.Label label = widget as global::Gtk.Label; diff --git a/src/client/web-process/web-process-extension.vala b/src/client/web-process/web-process-extension.vala index dc686350..110e21af 100644 --- a/src/client/web-process/web-process-extension.vala +++ b/src/client/web-process/web-process-extension.vala @@ -8,8 +8,8 @@ /** * Initialises GearyWebExtension for WebKit web processes. */ -public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension extension, - Variant data) { +public void webkit_web_process_extension_initialize_with_user_data(WebKit.WebProcessExtension extension, + Variant data) { bool logging_enabled = data.get_boolean(); Geary.Logging.init(); @@ -26,7 +26,7 @@ public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension e } /** - * A WebExtension that manages Geary-specific behaviours in web processes. + * A WebProcessExtension that manages Geary-specific behaviours in web processes. */ public class GearyWebExtension : Object { @@ -43,15 +43,17 @@ public class GearyWebExtension : Object { private const string EXTENSION_CLASS_SEND = "send"; private const string EXTENSION_CLASS_ALLOW_REMOTE_LOAD = "allowRemoteResourceLoad"; - private WebKit.WebExtension extension; + private WebKit.WebProcessExtension extension; - public GearyWebExtension(WebKit.WebExtension extension) { + public GearyWebExtension(WebKit.WebProcessExtension extension) { this.extension = extension; extension.page_created.connect(on_page_created); WebKit.ScriptWorld.get_default().window_object_cleared.connect(on_window_object_cleared); } + //XXX GTK4 - it seems this is no longer possible? +#if 0 private void on_console_message(WebKit.WebPage page, WebKit.ConsoleMessage message) { string source = message.get_source_id(); @@ -63,6 +65,7 @@ public class GearyWebExtension : Object { message.get_text() ); } +#endif private bool on_send_request(WebKit.WebPage page, WebKit.URIRequest request, @@ -128,9 +131,10 @@ public class GearyWebExtension : Object { ); } - private void on_page_created(WebKit.WebExtension extension, + private void on_page_created(WebKit.WebProcessExtension extension, WebKit.WebPage page) { - page.console_message_sent.connect(on_console_message); + //XXX GTK4 + // page.console_message_sent.connect(on_console_message); page.send_request.connect(on_send_request); page.user_message_received.connect(on_page_message_received); } diff --git a/src/console/imap-console.ui b/src/console/imap-console.ui new file mode 100644 index 00000000..d002f826 --- /dev/null +++ b/src/console/imap-console.ui @@ -0,0 +1,51 @@ + + + + diff --git a/src/console/main.vala b/src/console/main.vala index 39f4b0ca..2c778142 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -12,12 +12,13 @@ errordomain CommandException { const int IMAP_TIMEOUT_SEC = 60 * 15; -class ImapConsole : Gtk.Window { +[GtkTemplate (ui = "/org/gnome/GearyConsole/imap-console.ui")] +class ImapConsole : Adw.ApplicationWindow { private const int KEEPALIVE_SEC = 60 * 10; - private Gtk.TextView console = new Gtk.TextView(); - private Gtk.Entry cmdline = new Gtk.Entry(); - private Gtk.Statusbar statusbar = new Gtk.Statusbar(); + [GtkChild] private unowned Gtk.TextView console; + [GtkChild] private unowned Gtk.Entry cmdline; + [GtkChild] private unowned Gtk.Statusbar statusbar; private uint statusbar_ctx = 0; private uint statusbar_msg_id = 0; @@ -27,41 +28,26 @@ class ImapConsole : Gtk.Window { Geary.Imap.Tag, Geary.Imap.StatusResponse>(); private Geary.Nonblocking.Event recvd_response_event = new Geary.Nonblocking.Event(); - public ImapConsole() { - title = "IMAP Console"; - destroy.connect(() => { Gtk.main_quit(); }); - set_default_size(800, 600); - - Gtk.Box layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 4); - - console.editable = false; - Gtk.ScrolledWindow scrolled_console = new Gtk.ScrolledWindow(null, null); - scrolled_console.add(console); - scrolled_console.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); - layout.pack_start(scrolled_console, true, true, 0); - - cmdline.activate.connect(on_activate); - layout.pack_start(cmdline, false, false, 0); - - statusbar_ctx = statusbar.get_context_id("status"); - layout.pack_end(statusbar, false, false, 0); - - add(layout); + public ImapConsole(Adw.Application app) { + Object(application: app); + this.cmdline.activate.connect(on_activate); + this.statusbar_ctx = statusbar.get_context_id("status"); cmdline.grab_focus(); } - private void on_activate() { + [GtkCallback] + private void on_activate(Gtk.Entry cmdline) { exec(cmdline.buffer.text); cmdline.buffer.delete_text(0, -1); } private void clear_status() { - if (statusbar_msg_id == 0) + if (this.statusbar_msg_id == 0) return; - statusbar.remove(statusbar_ctx, statusbar_msg_id); - statusbar_msg_id = 0; + this.statusbar.remove(this.statusbar_ctx, this.statusbar_msg_id); + this.statusbar_msg_id = 0; } private void status(string text) { @@ -71,7 +57,7 @@ class ImapConsole : Gtk.Window { if (!msg.has_suffix(".") && !msg.has_prefix("usage")) msg += "."; - statusbar_msg_id = statusbar.push(statusbar_ctx, msg); + this.statusbar_msg_id = this.statusbar.push(this.statusbar_ctx, msg); } private void exception(Error err) { @@ -606,7 +592,8 @@ class ImapConsole : Gtk.Window { } private void quit(string cmd, string[] args) throws Error { - Gtk.main_quit(); + GLib.Application app = GLib.Application.get_default(); + app.quit(); } private bool keepalive_on = false; @@ -692,14 +679,16 @@ class ImapConsole : Gtk.Window { } } -void main(string[] args) { - Gtk.init(ref args); - +int main(string[] args) { Geary.Logging.init(); Geary.Logging.log_to(stdout); - ImapConsole console = new ImapConsole(); - console.show_all(); + Adw.Application app = new Adw.Application(null, GLib.ApplicationFlags.DEFAULT_FLAGS); - Gtk.main(); + app.activate.connect((gapp) => { + ImapConsole console = new ImapConsole(app); + console.present(); + }); + + return app.run(args); } diff --git a/src/console/meson.build b/src/console/meson.build index 0fe1b6f8..79066cde 100644 --- a/src/console/meson.build +++ b/src/console/meson.build @@ -7,12 +7,17 @@ console_dependencies = [ gtk, gee, gmime, - webkit2gtk, + libadwaita, + webkitgtk, engine_dep, ] +console_resources = gnome.compile_resources('org.gnome.GearyConsole', + files('org.gnome.GearyConsole.gresource.xml'), +) + console = executable('geary-console', - console_sources, + [ console_sources, console_resources ], dependencies: console_dependencies, vala_args: geary_vala_args, c_args: geary_c_args, diff --git a/src/console/org.gnome.GearyConsole.gresource.xml b/src/console/org.gnome.GearyConsole.gresource.xml new file mode 100644 index 00000000..eaaca769 --- /dev/null +++ b/src/console/org.gnome.GearyConsole.gresource.xml @@ -0,0 +1,6 @@ + + + + imap-console.ui + + diff --git a/src/engine/api/geary-credentials.vala b/src/engine/api/geary-credentials.vala index f44ad146..68400d49 100644 --- a/src/engine/api/geary-credentials.vala +++ b/src/engine/api/geary-credentials.vala @@ -94,6 +94,27 @@ public class Geary.Credentials : BaseObject, Gee.Hashable { ); } + public string to_string() { + switch (this) { + case NONE: + // Translators: ComboBox value for source of SMTP + // authentication credentials (none) when adding a new + // account + return _("No login needed"); + case USE_INCOMING: + // Translators: ComboBox value for source of SMTP + // authentication credentials (use IMAP) when adding a new + // account + return _("Use same login as receiving"); + case CUSTOM: + // Translators: ComboBox value for source of SMTP + // authentication credentials (custom) when adding a new + // account + return _("Use a different login"); + } + return_val_if_reached(""); + } + } diff --git a/src/engine/api/geary-service-information.vala b/src/engine/api/geary-service-information.vala index 6df8fb49..0dfd68b9 100644 --- a/src/engine/api/geary-service-information.vala +++ b/src/engine/api/geary-service-information.vala @@ -76,6 +76,21 @@ public enum Geary.TlsNegotiationMethod { ); } + /** + * Returns a user-displayable string of the enum value + */ + public unowned string to_string() { + switch (this) { + case Geary.TlsNegotiationMethod.NONE: + return _("None"); + case Geary.TlsNegotiationMethod.START_TLS: + return _("StartTLS"); + case Geary.TlsNegotiationMethod.TRANSPORT: + return _("TLS"); + } + + return_val_if_reached(""); + } } diff --git a/src/engine/util/util-logging.vala b/src/engine/util/util-logging.vala index 8f7895f9..09917327 100644 --- a/src/engine/util/util-logging.vala +++ b/src/engine/util/util-logging.vala @@ -621,7 +621,7 @@ public class Geary.Logging.State { * this by calling {@link get_earliest_record} and then {get_next}, * and can be notified of new records via {@link set_log_listener}. */ -public class Geary.Logging.Record { +public class Geary.Logging.Record : GLib.Object { /** The GLib domain of the log message, if any. */ diff --git a/src/mailer/meson.build b/src/mailer/meson.build index 39f97bb5..e319261b 100644 --- a/src/mailer/meson.build +++ b/src/mailer/meson.build @@ -6,7 +6,7 @@ mailer_dependencies = [ config_dep, gee, gmime, - webkit2gtk, + webkitgtk, engine_dep, ] diff --git a/src/meson.build b/src/meson.build index 52d6e1f8..198f9dc7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -79,7 +79,7 @@ web_process = library('geary-web-process', engine_dep, gee, gmime, - webkit2gtk_web_extension, + webkitgtk_web_extension, ], vala_args: geary_vala_args, c_args: geary_c_args, @@ -98,7 +98,6 @@ bin_sources += [ ] bin_dependencies = [ folks, - gdk, client_dep, engine_dep, gee, @@ -106,10 +105,10 @@ bin_dependencies = [ goa, gtk, javascriptcoregtk, - libhandy, + libadwaita, libmath, libpeas, - webkit2gtk, + webkitgtk, ] bin = executable('geary', @@ -126,31 +125,27 @@ valadoc_dependencies = [ enchant, folks, gcr, - gdk, gee, gio, glib, gmime, goa, - gspell, gtk, javascriptcoregtk, json_glib, - libhandy, + libadwaita, libpeas, libsecret, + libspelling, libxml, sqlite, - webkit2gtk + webkitgtk, ] valadoc_vapi_dirs = [ vapi_dir, meson.current_build_dir() ] -if libhandy_vapi != '' - valadoc_vapi_dirs += libhandy_vapi -endif # Hopefully Meson will get baked-in valadoc support, so we don't have # to resort to these kinds of hacks any more. See @@ -159,10 +154,10 @@ endif valadoc_dep_args = [] foreach dep : valadoc_dependencies valadoc_dep_args += '--pkg' - if dep != libhandy + if dep != libadwaita valadoc_dep_args += dep.name() else - valadoc_dep_args += 'libhandy-1' + valadoc_dep_args += 'libadwaita-1' endif endforeach valadoc_dep_args += [ '--pkg', 'config' ] diff --git a/subprojects/libhandy.wrap b/subprojects/libhandy.wrap deleted file mode 100644 index 2dfa9777..00000000 --- a/subprojects/libhandy.wrap +++ /dev/null @@ -1,5 +0,0 @@ -[wrap-git] -directory = libhandy -url = https://gitlab.gnome.org/GNOME/libhandy.git -revision = 1.2.1 - diff --git a/test/client/components/components-web-view-test-case.vala b/test/client/components/components-web-view-test-case.vala index 5558889f..f264a2d0 100644 --- a/test/client/components/components-web-view-test-case.vala +++ b/test/client/components/components-web-view-test-case.vala @@ -21,9 +21,7 @@ public abstract class Components.WebViewTestCase : TestCase { WebView.init_web_context( this.config, - File.new_for_path(Config.BUILD_ROOT_DIR).get_child("src"), - File.new_for_path("/tmp"), // XXX use something better here - false // https://bugs.webkit.org/show_bug.cgi?id=213174 + File.new_for_path(Config.BUILD_ROOT_DIR).get_child("src") ); try { WebView.load_resources(GLib.File.new_for_path("/tmp")); @@ -31,7 +29,9 @@ public abstract class Components.WebViewTestCase : TestCase { GLib.assert_not_reached(); } - this.test_view = set_up_test_view(); + this.test_view = set_up_test_view( + File.new_for_path("/tmp") // XXX use something better here + ); } protected override void tear_down() { @@ -39,13 +39,15 @@ public abstract class Components.WebViewTestCase : TestCase { this.test_view = null; } - protected abstract V set_up_test_view(); + protected abstract V set_up_test_view(GLib.File cache_dir); protected virtual void load_body_fixture(string html = "") { WebView client_view = (WebView) this.test_view; client_view.load_html_headless(html); + + unowned GLib.MainContext? main_context = GLib.MainContext.get_thread_default(); while (!client_view.is_content_loaded) { - Gtk.main_iteration(); + main_context.iteration(true); } } diff --git a/test/client/components/components-web-view-test.vala b/test/client/components/components-web-view-test.vala index 403576b5..d5e12d02 100644 --- a/test/client/components/components-web-view-test.vala +++ b/test/client/components/components-web-view-test.vala @@ -20,10 +20,10 @@ public class Components.WebViewTest : TestCase { config.enable_debug = true; WebView.init_web_context( config, - File.new_for_path(Config.BUILD_ROOT_DIR).get_child("src"), - File.new_for_path("/tmp"), // XXX use something better here - false // https://bugs.webkit.org/show_bug.cgi?id=213174 + File.new_for_path(Config.BUILD_ROOT_DIR).get_child("src") ); + //XXX GTK4 - we used to disable sandboxing here, because of + // https://bugs.webkit.org/show_bug.cgi?id=213174 } public void load_resources() throws GLib.Error { diff --git a/test/client/composer/composer-web-view-test.vala b/test/client/composer/composer-web-view-test.vala index ac3a1a66..d9584d19 100644 --- a/test/client/composer/composer-web-view-test.vala +++ b/test/client/composer/composer-web-view-test.vala @@ -51,10 +51,14 @@ public class Composer.WebViewTest : Components.WebViewTestCase assert(new WebView.EditContext("0;;;12;").font_size == 12); - assert(new WebView.EditContext("0;;;;rgb(0, 0, 0)").font_color == Util.Gtk.rgba(0, 0, 0, 1)); - assert(new WebView.EditContext("0;;;;rgb(255, 0, 0)").font_color == Util.Gtk.rgba(1, 0, 0, 1)); - assert(new WebView.EditContext("0;;;;rgb(0, 255, 0)").font_color == Util.Gtk.rgba(0, 1, 0, 1)); - assert(new WebView.EditContext("0;;;;rgb(0, 0, 255)").font_color == Util.Gtk.rgba(0, 0, 1, 1)); + Gdk.RGBA black = { 0, 0, 0, 1 }; + assert(new WebView.EditContext("0;;;;rgb(0, 0, 0)").font_color == black); + Gdk.RGBA red = { 1, 0, 0, 1 }; + assert(new WebView.EditContext("0;;;;rgb(255, 0, 0)").font_color == red); + Gdk.RGBA green = { 0, 1, 0, 1 }; + assert(new WebView.EditContext("0;;;;rgb(0, 255, 0)").font_color == green); + Gdk.RGBA blue = { 0, 0, 1, 1 }; + assert(new WebView.EditContext("0;;;;rgb(0, 0, 255)").font_color == blue); } public void get_html() throws GLib.Error { @@ -116,8 +120,8 @@ at least. Really long, long, long, long, long long, long long, long long, long.< this.test_view.get_text.begin(this.async_completion); try { assert(this.test_view.get_text.end(async_result()) == -"""A long, long, long, long, long, long para. Well, longer than -MAX_BREAKABLE_LEN at least. Really long, long, long, long, long long, +"""A long, long, long, long, long, long para. Well, longer than +MAX_BREAKABLE_LEN at least. Really long, long, long, long, long long, long long, long long, long. @@ -139,11 +143,11 @@ at least. Really long, long, long, long, long long, long long, long long, long.< this.test_view.get_text.begin(this.async_completion); try { assert(this.test_view.get_text.end(async_result()) == -"""> A long, long, long, long, long, long line. Well, longer than +"""> A long, long, long, long, long, long line. Well, longer than > MAX_BREAKABLE_LEN at least. -> -A long, long, long, long, long, long para. Well, longer than -MAX_BREAKABLE_LEN at least. Really long, long, long, long, long long, +> +A long, long, long, long, long, long para. Well, longer than +MAX_BREAKABLE_LEN at least. Really long, long, long, long, long long, long long, long long, long. @@ -167,16 +171,16 @@ long long, long long, long. try { assert(this.test_view.get_text.end(async_result()) == """On Sun, Jan 1, 2017 at 9:55 PM, Michael Gratton wrote: -> long, long, long, long, long, long, long, long, long, long, long, -> long, long, long, long, long, long, long, long, long, long, long, -> long, long, long, long, long, long, long, long, long, long, long, -> long, long, long, long, long, long, long, long, long, long, long, +> long, long, long, long, long, long, long, long, long, long, long, +> long, long, long, long, long, long, long, long, long, long, long, +> long, long, long, long, long, long, long, long, long, long, long, +> long, long, long, long, long, long, long, long, long, long, long, > long, long, long, long, long, -long, long, long, long, long, long, long, long, long, long, long, long, -long, long, long, long, long, long, long, long, long, long, long, long, -long, long, long, long, long, long, long, long, long, long, long, long, -long, long, long, long, long, long, long, long, long, long, long, long, +long, long, long, long, long, long, long, long, long, long, long, long, +long, long, long, long, long, long, long, long, long, long, long, long, +long, long, long, long, long, long, long, long, long, long, long, long, +long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, long, @@ -253,14 +257,16 @@ long, long, long, long, long, long, long, long, long, long, assert_false(SIG2 in html, "Signature 2 still present"); } - protected override Composer.WebView set_up_test_view() { - return new Composer.WebView(this.config); + protected override Composer.WebView set_up_test_view(GLib.File cache_dir) { + return new Composer.WebView(this.config, cache_dir); } protected override void load_body_fixture(string html = "") { this.test_view.load_html_headless(html, "", false, false); + + unowned GLib.MainContext? main_context = GLib.MainContext.get_thread_default(); while (this.test_view.is_loading) { - Gtk.main_iteration(); + main_context.iteration(true); } } diff --git a/test/client/composer/composer-widget-test.vala b/test/client/composer/composer-widget-test.vala index a97fd048..6ad78208 100644 --- a/test/client/composer/composer-widget-test.vala +++ b/test/client/composer/composer-widget-test.vala @@ -79,6 +79,14 @@ public class Composer.WidgetTest : TestCase { } } + internal GLib.File get_web_cache_dir() { + try { + return File.new_for_path(DirUtils.make_tmp("geary-widget-test-XXXXXX")); + } catch (GLib.Error err) { + // We'll assert later + } + GLib.assert_not_reached(); + } } diff --git a/test/js/components-page-state-test.vala b/test/js/components-page-state-test.vala index beedf3fa..cc1dbc8a 100644 --- a/test/js/components-page-state-test.vala +++ b/test/js/components-page-state-test.vala @@ -10,8 +10,8 @@ class Components.PageStateTest : WebViewTestCase { private class TestWebView : Components.WebView { - public TestWebView(Application.Configuration config) { - base(config); + public TestWebView(Application.Configuration config, GLib.File cache_dir) { + base(config, cache_dir); } public new async void call_void(Util.JS.Callable callable) @@ -141,7 +141,7 @@ class Components.PageStateTest : WebViewTestCase { } } - protected override WebView set_up_test_view() { + protected override WebView set_up_test_view(GLib.File cache_dir) { WebKit.UserScript test_script; test_script = new WebKit.UserScript( "var geary = new PageState()", @@ -151,7 +151,7 @@ class Components.PageStateTest : WebViewTestCase { null ); - WebView view = new TestWebView(this.config); + WebView view = new TestWebView(this.config, cache_dir); view.get_user_content_manager().add_script(test_script); return view; } diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala index c8e84752..ce4cc21c 100644 --- a/test/js/composer-page-state-test.vala +++ b/test/js/composer-page-state-test.vala @@ -412,8 +412,8 @@ I can send email through smtp.gmail.com:587 or through { - ret = Test.run(); - Gtk.main_quit(); - return false; - }); + ret = Test.run(); + loop.quit(); + return Source.REMOVE; + }); - Gtk.main(); + loop.run(); return ret; } diff --git a/test/test-js.vala b/test/test-js.vala index c3e0a7d4..10904e8d 100644 --- a/test/test-js.vala +++ b/test/test-js.vala @@ -24,10 +24,9 @@ int main(string[] args) { GLib.Intl.setlocale(LocaleCategory.ALL, "C.UTF-8"); - Gtk.init(ref args); + Gtk.init(); Test.init(ref args); - IconFactory.init(GLib.File.new_for_path(Config.SOURCE_ROOT_DIR)); Geary.RFC822.init(); Geary.HTML.init(); Geary.Logging.init(); @@ -52,13 +51,15 @@ int main(string[] args) { unowned TestSuite root = TestSuite.get_root(); root.add_suite((owned) js); + MainLoop loop = new MainLoop(); + int ret = -1; Idle.add(() => { - ret = Test.run(); - Gtk.main_quit(); - return false; - }); + ret = Test.run(); + loop.quit(); + return Source.REMOVE; + }); - Gtk.main(); + loop.run(); return ret; } diff --git a/ui/accounts-editor-account-list-row.ui b/ui/accounts-editor-account-list-row.ui new file mode 100644 index 00000000..96857c59 --- /dev/null +++ b/ui/accounts-editor-account-list-row.ui @@ -0,0 +1,41 @@ + + + + diff --git a/ui/accounts-mailbox-editor-dialog.ui b/ui/accounts-mailbox-editor-dialog.ui new file mode 100644 index 00000000..49c739b1 --- /dev/null +++ b/ui/accounts-mailbox-editor-dialog.ui @@ -0,0 +1,81 @@ + + + + diff --git a/ui/accounts-service-information-widget.ui b/ui/accounts-service-information-widget.ui new file mode 100644 index 00000000..4e337f0b --- /dev/null +++ b/ui/accounts-service-information-widget.ui @@ -0,0 +1,64 @@ + + + + diff --git a/ui/accounts-tls-combo-row.ui b/ui/accounts-tls-combo-row.ui new file mode 100644 index 00000000..66003356 --- /dev/null +++ b/ui/accounts-tls-combo-row.ui @@ -0,0 +1,18 @@ + + + + diff --git a/ui/accounts_editor.ui b/ui/accounts_editor.ui index 717b0918..fd42f1d8 100644 --- a/ui/accounts_editor.ui +++ b/ui/accounts_editor.ui @@ -1,54 +1,19 @@ - - -