diff --git a/data/io.elementary.mail.gschema.xml b/data/io.elementary.mail.gschema.xml index edc4f9709..cfdafb461 100644 --- a/data/io.elementary.mail.gschema.xml +++ b/data/io.elementary.mail.gschema.xml @@ -7,15 +7,15 @@ Whether the window was maximized on last run Whether the window was maximized on last run - - (-1, -1) - Window position - Most recent window position (x, y) - - - (1024, 750) - Most recent window size - Most recent window size (width, height) + + 1024 + Most recent window width + Most recent window width + + + 750 + Most recent window height + Most recent window height 190 diff --git a/meson.build b/meson.build index 93fe3a47c..32284e43f 100644 --- a/meson.build +++ b/meson.build @@ -11,35 +11,31 @@ add_project_arguments(['--vapidir', join_paths(meson.current_source_dir(), 'vapi glib_dep = dependency('glib-2.0') gobject_dep = dependency('gobject-2.0') -granite_dep = dependency('granite', version: '>= 6.0.0') +granite_dep = dependency('granite-7', version: '>=7.1') gee_dep = dependency('gee-0.8') -handy_dep = dependency('libhandy-1', version: '>=1.1.90') +adw_dep = dependency('libadwaita-1') +gtk_dep = dependency('gtk4') camel_dep = dependency('camel-1.2', version: '>= 3.28') -libedataserver_dep = dependency('libedataserver-1.2', version: '>= 3.28') -libedataserverui_dep = dependency('libedataserverui-1.2', version: '>= 3.28') -if libedataserverui_dep.version().version_compare('>=3.45.1') - add_project_arguments('--define=HAS_SOUP_3', language: 'vala') - webkit2_dep = dependency('webkit2gtk-4.1') - webkit2_web_extension_dep = dependency('webkit2gtk-web-extension-4.1') -else - webkit2_dep = dependency('webkit2gtk-4.0', version: '>=2.28') - webkit2_web_extension_dep = dependency('webkit2gtk-web-extension-4.0', version: '>=2.28') -endif +libedataserver_dep = dependency('libedataserver-1.2', version: '>=3.45.1') +libedataserverui_dep = dependency('libedataserverui4-1.0', version: '>= 3.46.4') +webkit_dep = dependency('webkitgtk-6.0') +webkit_web_extension_dep = dependency('webkitgtk-web-process-extension-6.0') folks_dep = dependency('folks') m_dep = meson.get_compiler('c').find_library('m') -webkit2_extension_path = join_paths(get_option('prefix'), get_option('libdir'), meson.project_name(), 'webkit2') +webkit_extension_path = join_paths(get_option('prefix'), get_option('libdir'), meson.project_name(), 'webkit2') dependencies = [ glib_dep, gobject_dep, granite_dep, gee_dep, - handy_dep, + adw_dep, + gtk_dep, camel_dep, libedataserver_dep, libedataserverui_dep, - webkit2_dep, + webkit_dep, folks_dep, m_dep ] @@ -54,12 +50,11 @@ extension_dependencies = [ glib_dep, gobject_dep, gee_dep, - webkit2_web_extension_dep + webkit_web_extension_dep ] add_global_arguments([ '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), - '-DHANDY_USE_UNSTABLE_API' ], language:'c' ) diff --git a/src/Application.vala b/src/Application.vala index 13e6c9f46..197142a93 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -51,7 +51,6 @@ public class Mail.Application : Gtk.Application { string to = null; try { -#if HAS_SOUP_3 GLib.Uri? mailto= null; try { mailto = GLib.Uri.parse (mailto_uri, GLib.UriFlags.NONE); @@ -76,26 +75,6 @@ public class Mail.Application : Gtk.Application { new Composer (to, mailto.get_query ()).present (); }); } -#else - Soup.URI mailto = new Soup.URI (mailto_uri); - if (mailto == null) { - throw new OptionError.BAD_VALUE ("Argument is not a URL."); - } - - if (mailto.scheme != "mailto") { - throw new OptionError.BAD_VALUE ("Cannot open non-mailto: URL"); - } - - to = Soup.URI.decode (mailto.path); - - if (main_window.is_session_started) { - new Composer (to, mailto.query).present (); - } else { - main_window.session_started.connect (() => { - new Composer (to, mailto.query).present (); - }); - } -#endif } catch (OptionError e) { warning ("Argument parsing error. %s", e.message); } @@ -107,8 +86,6 @@ public class Mail.Application : Gtk.Application { protected override void startup () { base.startup (); - Hdy.init (); - var granite_settings = Granite.Settings.get_default (); var gtk_settings = Gtk.Settings.get_default (); @@ -120,7 +97,7 @@ public class Mail.Application : Gtk.Application { var css_provider = new Gtk.CssProvider (); css_provider.load_from_resource ("io/elementary/mail/application.css"); - Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); var quit_action = new SimpleAction ("quit", null); quit_action.activate.connect (() => { @@ -155,24 +132,6 @@ public class Mail.Application : Gtk.Application { if (main_window == null) { main_window = new MainWindow (this); add_window (main_window); - - int window_x, window_y; - var rect = Gtk.Allocation (); - - settings.get ("window-position", "(ii)", out window_x, out window_y); - settings.get ("window-size", "(ii)", out rect.width, out rect.height); - - if (window_x != -1 || window_y != -1) { - main_window.move (window_x, window_y); - } - - main_window.set_allocation (rect); - - if (settings.get_boolean ("window-maximized")) { - main_window.maximize (); - } - - main_window.show_all (); } main_window.present (); diff --git a/src/Composer.vala b/src/Composer.vala index da7b0cbc2..71d177b7b 100644 --- a/src/Composer.vala +++ b/src/Composer.vala @@ -5,13 +5,13 @@ * Authored by: David Hewitt */ -public class Mail.Composer : Hdy.ApplicationWindow { +public class Mail.Composer : Gtk.ApplicationWindow { public signal void finished (); private const string ACTION_GROUP_PREFIX = "win"; private const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; - private const string ACTION_ADD_ATTACHMENT= "add-attachment"; + private const string ACTION_ADD_ATTACHMENT= "append-attachment"; private const string ACTION_BOLD = "bold"; private const string ACTION_ITALIC = "italic"; private const string ACTION_UNDERLINE = "underline"; @@ -36,7 +36,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { private Gtk.Revealer cc_revealer; private Gtk.Revealer bcc_revealer; private Gtk.ToggleButton cc_button; - private Granite.Widgets.OverlayBar message_url_overlay; + private Granite.OverlayBar message_url_overlay; private Gtk.ComboBoxText from_combo; private Gtk.Entry subject_val; @@ -90,17 +90,14 @@ public class Mail.Composer : Hdy.ApplicationWindow { } } - var headerbar = new Hdy.HeaderBar () { - has_subtitle = false, - show_close_button = true - }; - headerbar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - headerbar.get_style_context ().add_class ("default-decoration"); + var headerbar = new Gtk.HeaderBar (); + headerbar.add_css_class (Granite.STYLE_CLASS_FLAT); + headerbar.add_css_class ("default-decoration"); var from_label = new Gtk.Label (_("From:")) { xalign = 1 }; - from_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + from_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); from_combo = new Gtk.ComboBoxText () { hexpand = true @@ -109,8 +106,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { var from_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) { margin_bottom = 6 }; - from_box.add (from_label); - from_box.add (from_combo); + from_box.append (from_label); + from_box.append (from_combo); var from_revealer = new Gtk.Revealer () { child = from_box @@ -119,12 +116,12 @@ public class Mail.Composer : Hdy.ApplicationWindow { var to_label = new Gtk.Label (_("To:")) { xalign = 1 }; - to_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + to_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var subject_label = new Gtk.Label (_("Subject:")) { xalign = 1 }; - subject_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + subject_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); to_val = new Gtk.Entry () { hexpand = true @@ -134,15 +131,15 @@ public class Mail.Composer : Hdy.ApplicationWindow { var bcc_button = new Gtk.ToggleButton.with_label (_("Bcc")); - var to_grid = new EntryGrid (); - to_grid.add (to_val); - to_grid.add (cc_button); - to_grid.add (bcc_button); + var to_box = new EntryBox (); + to_box.append (to_val); + to_box.append (cc_button); + to_box.append (bcc_button); var cc_label = new Gtk.Label (_("Cc:")) { xalign = 1 }; - cc_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + cc_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); cc_val = new Gtk.Entry () { hexpand = true @@ -151,16 +148,17 @@ public class Mail.Composer : Hdy.ApplicationWindow { var cc_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) { margin_top = 6 }; - cc_box.add (cc_label); - cc_box.add (cc_val); + cc_box.append (cc_label); + cc_box.append (cc_val); - cc_revealer = new Gtk.Revealer (); - cc_revealer.add (cc_box); + cc_revealer = new Gtk.Revealer () { + child = cc_box + }; var bcc_label = new Gtk.Label (_("Bcc:")) { xalign = 1 }; - bcc_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + bcc_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); bcc_val = new Gtk.Entry () { hexpand = true @@ -169,11 +167,12 @@ public class Mail.Composer : Hdy.ApplicationWindow { var bcc_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) { margin_top = 6 }; - bcc_box.add (bcc_label); - bcc_box.add (bcc_val); + bcc_box.append (bcc_label); + bcc_box.append (bcc_val); - bcc_revealer = new Gtk.Revealer (); - bcc_revealer.add (bcc_box); + bcc_revealer = new Gtk.Revealer () { + child = bcc_box + }; subject_val = new Gtk.Entry () { margin_top = 6 @@ -199,7 +198,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { }; recipient_grid.attach (from_revealer, 0, 0, 2); recipient_grid.attach (to_label, 0, 1); - recipient_grid.attach (to_grid, 1, 1); + recipient_grid.attach (to_box, 1, 1); recipient_grid.attach (cc_revealer, 0, 2, 2); recipient_grid.attach (bcc_revealer, 0, 3, 2); recipient_grid.attach (subject_label, 0, 4); @@ -208,21 +207,21 @@ public class Mail.Composer : Hdy.ApplicationWindow { var bold = new Gtk.ToggleButton () { action_name = ACTION_PREFIX + ACTION_BOLD, action_target = ACTION_BOLD, - image = new Gtk.Image.from_icon_name ("format-text-bold-symbolic", Gtk.IconSize.MENU), + icon_name = "format-text-bold-symbolic", tooltip_markup = Granite.markup_accel_tooltip ({"B"}, _("Bold")) }; var italic = new Gtk.ToggleButton () { action_name = ACTION_PREFIX + ACTION_ITALIC, action_target = ACTION_ITALIC, - image = new Gtk.Image.from_icon_name ("format-text-italic-symbolic", Gtk.IconSize.MENU), + icon_name = "format-text-italic-symbolic", tooltip_markup = Granite.markup_accel_tooltip ({"I"}, _("Italic")) }; var underline = new Gtk.ToggleButton () { action_name = ACTION_PREFIX + ACTION_UNDERLINE, action_target = ACTION_UNDERLINE, - image = new Gtk.Image.from_icon_name ("format-text-underline-symbolic", Gtk.IconSize.MENU), + icon_name = "format-text-underline-symbolic", tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (Action.print_detailed_name (ACTION_PREFIX + ACTION_UNDERLINE, ACTION_UNDERLINE)), _("Underline") @@ -232,14 +231,14 @@ public class Mail.Composer : Hdy.ApplicationWindow { var strikethrough = new Gtk.ToggleButton () { action_name = ACTION_PREFIX + ACTION_STRIKETHROUGH, action_target = ACTION_STRIKETHROUGH, - image = new Gtk.Image.from_icon_name ("format-text-strikethrough-symbolic", Gtk.IconSize.MENU), + icon_name = "format-text-strikethrough-symbolic", tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (Action.print_detailed_name (ACTION_PREFIX + ACTION_STRIKETHROUGH, ACTION_STRIKETHROUGH)), _("Strikethrough") ) }; - var clear_format = new Gtk.Button.from_icon_name ("format-text-clear-formatting-symbolic", Gtk.IconSize.MENU) { + var clear_format = new Gtk.Button.from_icon_name ("format-text-clear-formatting-symbolic") { action_name = ACTION_PREFIX + ACTION_REMOVE_FORMAT, tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (ACTION_PREFIX + ACTION_REMOVE_FORMAT), @@ -248,13 +247,13 @@ public class Mail.Composer : Hdy.ApplicationWindow { }; var formatting_buttons = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); - formatting_buttons.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED); - formatting_buttons.add (bold); - formatting_buttons.add (italic); - formatting_buttons.add (underline); - formatting_buttons.add (strikethrough); + formatting_buttons.add_css_class (Granite.STYLE_CLASS_LINKED); + formatting_buttons.append (bold); + formatting_buttons.append (italic); + formatting_buttons.append (underline); + formatting_buttons.append (strikethrough); - var link = new Gtk.Button.from_icon_name ("insert-link-symbolic", Gtk.IconSize.MENU) { + var link = new Gtk.Button.from_icon_name ("insert-link-symbolic") { action_name = ACTION_PREFIX + ACTION_INSERT_LINK, tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (ACTION_PREFIX + ACTION_INSERT_LINK), @@ -266,9 +265,9 @@ public class Mail.Composer : Hdy.ApplicationWindow { margin_start = 6, margin_bottom = 6 }; - button_row.add (formatting_buttons); - button_row.add (clear_format ); - button_row.add (link); + button_row.append (formatting_buttons); + button_row.append (clear_format ); + button_row.append (link); web_view = new WebView (); try { @@ -285,9 +284,9 @@ public class Mail.Composer : Hdy.ApplicationWindow { homogeneous = true, selection_mode = Gtk.SelectionMode.NONE }; - attachment_box.get_style_context ().add_class (Gtk.STYLE_CLASS_VIEW); + attachment_box.add_css_class (Granite.STYLE_CLASS_VIEW); - var discard = new Gtk.Button.from_icon_name ("edit-delete-symbolic", Gtk.IconSize.MENU) { + var discard = new Gtk.Button.from_icon_name ("edit-delete-symbolic") { action_name = ACTION_PREFIX + ACTION_DISCARD, tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (ACTION_PREFIX + ACTION_DISCARD), @@ -295,7 +294,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { ) }; - var attach = new Gtk.Button.from_icon_name ("mail-attachment-symbolic", Gtk.IconSize.MENU) { + var attach = new Gtk.Button.from_icon_name ("mail-attachment-symbolic") { action_name = ACTION_PREFIX + ACTION_ADD_ATTACHMENT, tooltip_markup = Granite.markup_accel_tooltip ( application.get_accels_for_action (ACTION_PREFIX + ACTION_ADD_ATTACHMENT), @@ -303,9 +302,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { ) }; - var send = new Gtk.Button.from_icon_name ("mail-send-symbolic", Gtk.IconSize.MENU) { + var send = new Gtk.Button.from_icon_name ("mail-send-symbolic") { action_name = ACTION_PREFIX + ACTION_SEND, - always_show_image = true, label = _("Send"), margin_top = 6, margin_end = 0, @@ -316,38 +314,39 @@ public class Mail.Composer : Hdy.ApplicationWindow { application.get_accels_for_action (ACTION_PREFIX + ACTION_SEND) ) }; - send.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + send.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); var action_bar = new Gtk.ActionBar () { // Workaround styling issue margin_top = 1 }; - action_bar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + action_bar.add_css_class (Granite.STYLE_CLASS_FLAT); action_bar.pack_start (discard); action_bar.pack_start (attach); action_bar.pack_end (send); - var view_overlay = new Gtk.Overlay (); - view_overlay.add (web_view); - message_url_overlay = new Granite.Widgets.OverlayBar (view_overlay); - message_url_overlay.no_show_all = true; + var view_overlay = new Gtk.Overlay () { + child = web_view + }; + message_url_overlay = new Granite.OverlayBar (view_overlay); + message_url_overlay.visible = false; var main_box = new Gtk.Box (VERTICAL, 0); - main_box.add (headerbar); - main_box.add (recipient_grid); - main_box.add (button_row); - main_box.add (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); - main_box.add (view_overlay); - main_box.add (attachment_box); - main_box.add (action_bar); - + // main_box.append (headerbar); + main_box.append (recipient_grid); + main_box.append (button_row); + main_box.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + main_box.append (view_overlay); + main_box.append (attachment_box); + main_box.append (action_bar); + + titlebar = headerbar; default_height = 500; default_width = 680; title = _("New Message"); - add (main_box); - show_all (); + set_child (main_box); - delete_event.connect (() => { + close_request.connect (() => { save_draft.begin ((obj, res) => { if (!save_draft.end (res)) { finished (); @@ -366,7 +365,6 @@ public class Mail.Composer : Hdy.ApplicationWindow { from_revealer.reveal_child = from_combo.model.iter_n_children (null) > 1; bind_property ("has-recipients", send, "sensitive"); - bind_property ("title", headerbar, "title"); cc_button.clicked.connect (() => { cc_revealer.reveal_child = cc_button.active; @@ -403,18 +401,6 @@ public class Mail.Composer : Hdy.ApplicationWindow { has_recipients = to_val.text != ""; }); - to_val.get_style_context ().changed.connect (() => { - unowned Gtk.StyleContext to_grid_style_context = to_grid.get_style_context (); - var state = to_grid_style_context.get_state (); - if (to_val.has_focus) { - state |= Gtk.StateFlags.FOCUSED; - } else { - state ^= Gtk.StateFlags.FOCUSED; - } - - to_grid_style_context.set_state (state); - }); - if (to != null) { to_val.text = to; } @@ -426,11 +412,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { foreach (unowned string param in params) { var terms = param.split ("="); if (terms.length == 2) { -#if HAS_SOUP_3 result[terms[0].down ()] = (GLib.Uri.unescape_string (terms[1])); -#else - result[terms[0].down ()] = (Soup.URI.decode (terms[1])); -#endif } else { critical ("Invalid mailto URL"); } @@ -465,11 +447,10 @@ public class Mail.Composer : Hdy.ApplicationWindow { var file = path.has_prefix ("file://") ? File.new_for_uri (path) : File.new_for_path (path); var attachment = new Attachment (file); - attachment.margin = 3; + attachment.remove.connect (() => attachment_box.remove (attachment)); - attachment_box.add (attachment); + attachment_box.append (attachment); } - attachment_box.show_all (); } } } @@ -491,24 +472,27 @@ public class Mail.Composer : Hdy.ApplicationWindow { private void on_add_attachment () { var filechooser = new Gtk.FileChooserNative ( _("Choose a file"), - (Gtk.Window) get_toplevel (), + this, Gtk.FileChooserAction.OPEN, _("Attach"), _("Cancel") ); - if (filechooser.run () == Gtk.ResponseType.ACCEPT) { + filechooser.response.connect ((response) => { filechooser.hide (); - foreach (unowned File file in filechooser.get_files ()) { - var attachment = new Attachment (file); - attachment.margin = 3; + if (response == Gtk.ResponseType.ACCEPT) { + var files = filechooser.get_files (); + for (int i = 0; files.get_item (i) != null; i++) { + var attachment = new Attachment ((File)files.get_item (i)); + attachment.remove.connect (() => attachment_box.remove (attachment)); - attachment_box.add (attachment); + attachment_box.append (attachment); + } } - attachment_box.show_all (); - } + filechooser.destroy (); + }); - filechooser.destroy (); + filechooser.show (); } private void on_insert_link_clicked () { @@ -539,18 +523,13 @@ public class Mail.Composer : Hdy.ApplicationWindow { private void on_mouse_target_changed (WebKit.WebView web_view, WebKit.HitTestResult hit_test, uint mods) { if (hit_test.context_is_link ()) { var url = hit_test.get_link_uri (); -#if HAS_SOUP_3 var hover_url = url != null ? GLib.Uri.unescape_string (url) : null; -#else - var hover_url = url != null ? Soup.URI.decode (url) : null; -#endif if (hover_url == null) { message_url_overlay.hide (); } else { message_url_overlay.label = hover_url; - message_url_overlay.no_show_all = false; - message_url_overlay.show_all (); + message_url_overlay.show (); } } else { message_url_overlay.hide (); @@ -614,12 +593,12 @@ public class Mail.Composer : Hdy.ApplicationWindow { message_content = content_to_quote; unowned var to = message.get_recipients (Camel.RECIPIENT_TYPE_TO); - if (to != null) { + if (to.format () != null) { to_val.text = to.format (); } unowned var cc = message.get_recipients (Camel.RECIPIENT_TYPE_CC); - if (cc != null) { + if (cc.format () != null) { cc_val.text = cc.format (); if (cc_val.text.length > 0) { @@ -628,7 +607,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { } unowned var bcc = message.get_recipients (Camel.RECIPIENT_TYPE_BCC); - if (bcc != null) { + if (bcc.format () != null) { bcc_val.text = bcc.format (); if (bcc_val.text.length > 0) { @@ -640,7 +619,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { string date_format = _("%a, %b %-e, %Y at %-l:%M %p"); if (type == Type.REPLY || type == Type.REPLY_ALL) { var reply_to = message.get_reply_to (); - if (reply_to != null) { + if (reply_to.format () != null) { to_val.text = reply_to.format (); } else { to_val.text = message.get_from ().format (); @@ -730,7 +709,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { discard_dialog.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); var discard_anyway = discard_dialog.add_button (_("Delete Draft"), Gtk.ResponseType.ACCEPT); - discard_anyway.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + discard_anyway.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); discard_dialog.present (); discard_dialog.response.connect ((response) => { @@ -764,7 +743,7 @@ public class Mail.Composer : Hdy.ApplicationWindow { no_subject_dialog.add_button (_("Don't Send"), Gtk.ResponseType.CANCEL); var send_anyway = no_subject_dialog.add_button (_("Send Anyway"), Gtk.ResponseType.ACCEPT); - send_anyway.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + send_anyway.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); no_subject_dialog.present (); no_subject_dialog.response.connect ((response) => { @@ -793,7 +772,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { new ThemedIcon ("mail-send"), Gtk.ButtonsType.CLOSE ) { - badge_icon = new ThemedIcon ("dialog-warning") + badge_icon = new ThemedIcon ("dialog-warning"), + transient_for = this }; warning_dialog.present (); warning_dialog.response.connect (() => warning_dialog.destroy ()); @@ -809,7 +789,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { new ThemedIcon ("mail-send"), Gtk.ButtonsType.CLOSE ) { - badge_icon = new ThemedIcon ("dialog-error") + badge_icon = new ThemedIcon ("dialog-error"), + transient_for = this }; error_dialog.show_error_details (e.message); error_dialog.present (); @@ -869,15 +850,16 @@ public class Mail.Composer : Hdy.ApplicationWindow { body.set_boundary (null); body.add_part (part); - if (attachment_box.get_children ().length () > 0) { - foreach (unowned Gtk.Widget attachment in attachment_box.get_children ()) { - if (!(attachment is Attachment)) { - continue; - } - - unowned var attachment_obj = (Attachment)attachment; - body.add_part (attachment_obj.get_mime_part ()); + Gtk.Widget current_attachment = attachment_box.get_first_child (); + while (current_attachment != null) { + if (!(current_attachment is Attachment)) { + current_attachment = current_attachment.get_next_sibling (); + continue; } + + unowned var attachment_obj = (Attachment)current_attachment; + body.add_part (attachment_obj.get_mime_part ()); + current_attachment = current_attachment.get_next_sibling (); } var message = new Camel.MimeMessage (); @@ -898,6 +880,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { } private class Attachment : Gtk.FlowBoxChild { + public signal void remove (); + public GLib.FileInfo? info { private get; construct; } public GLib.File file { get; construct; } @@ -931,26 +915,32 @@ public class Mail.Composer : Hdy.ApplicationWindow { }; var size_label = new Gtk.Label ("(%s)".printf (GLib.format_size (info.get_size ()))); - size_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + size_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); - var remove_button = new Gtk.Button.from_icon_name ("process-stop-symbolic", Gtk.IconSize.SMALL_TOOLBAR); - - unowned Gtk.StyleContext remove_button_context = remove_button.get_style_context (); - remove_button_context.add_class (Gtk.STYLE_CLASS_FLAT); - remove_button_context.add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + var remove_button = new Gtk.Button.from_icon_name ("process-stop-symbolic"); + remove_button.add_css_class (Granite.STYLE_CLASS_FLAT); + remove_button.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); var box = new Gtk.Box (HORIZONTAL, 3) { - margin = 3 + margin_top = 3, + margin_bottom = 3, + margin_start = 3, + margin_end = 3 }; - box.add (image); - box.add (name_label); - box.add (size_label); - box.add (remove_button); + box.append (image); + box.append (name_label); + box.append (size_label); + box.append (remove_button); + + margin_top = 3; + margin_bottom = 3; + margin_start = 3; + margin_end = 3; - add (box); + set_child (box); remove_button.clicked.connect (() => { - destroy (); + remove (); }); } @@ -1001,14 +991,16 @@ public class Mail.Composer : Hdy.ApplicationWindow { } } - private class EntryGrid : Gtk.Grid { + private class EntryBox : Gtk.Box { static construct { - set_css_name (Gtk.STYLE_CLASS_ENTRY); + set_css_name ("entry"); } } public async bool save_draft () { - if (discard_draft || !web_view.body_html_changed) { + /* @TODO: Currently we always save (also empty drafts) maybe change that. + * Previously (gtk3) it only saved if the web view body html changed, but it's hard to detect that now or at least I did find an easy way */ + if (discard_draft) { return false; } @@ -1030,7 +1022,8 @@ public class Mail.Composer : Hdy.ApplicationWindow { new ThemedIcon ("mail-drafts"), Gtk.ButtonsType.CLOSE ) { - badge_icon = new ThemedIcon ("dialog-error") + badge_icon = new ThemedIcon ("dialog-error"), + transient_for = this }; error_dialog.show_error_details (e.message); error_dialog.present (); diff --git a/src/ConversationList/ConversationList.vala b/src/ConversationList/ConversationList.vala index 43e2bb285..7cb9a2af6 100644 --- a/src/ConversationList/ConversationList.vala +++ b/src/ConversationList/ConversationList.vala @@ -22,60 +22,38 @@ public class Mail.ConversationList : Gtk.Box { public signal void conversation_selected (Camel.FolderThreadNode? node); - public signal void conversation_focused (Camel.FolderThreadNode? node); private const int MARK_READ_TIMEOUT_SECONDS = 5; public Gee.Map folder_full_name_per_account { get; private set; } public Gee.HashMap folders { get; private set; } public Gee.HashMap folder_info_flags { get; private set; } - public Hdy.HeaderBar search_header { get; private set; } + public Gtk.HeaderBar search_header { get; private set; } private GLib.Cancellable? cancellable = null; private Gee.HashMap threads; private Gee.HashMap conversations; - private ConversationListStore list_store; private MoveHandler move_handler; - private VirtualizingListBox list_box; private Gtk.SearchEntry search_entry; private Granite.SwitchModelButton hide_read_switch; private Granite.SwitchModelButton hide_unstarred_switch; private Gtk.MenuButton filter_button; + private ConversationListStore list_store; + private Gtk.SingleSelection selection_model; + private Gtk.ListView list_view; + private Gtk.PopoverMenu context_menu; private Gtk.Stack refresh_stack; private uint mark_read_timeout_id = 0; construct { - orientation = VERTICAL; - get_style_context ().add_class (Gtk.STYLE_CLASS_VIEW); - conversations = new Gee.HashMap (); folders = new Gee.HashMap (); folder_info_flags = new Gee.HashMap (); threads = new Gee.HashMap (); - list_store = new ConversationListStore (); - list_store.set_sort_func (thread_sort_function); - list_store.set_filter_func (filter_function); move_handler = new MoveHandler (); - list_box = new VirtualizingListBox () { - activate_on_single_click = true, - model = list_store - }; - list_box.factory_func = (item, old_widget) => { - ConversationListItem? row = null; - if (old_widget != null) { - row = old_widget as ConversationListItem; - } else { - row = new ConversationListItem (); - } - - row.assign ((ConversationItemModel)item); - row.show_all (); - return row; - }; - var application_instance = (Gtk.Application) GLib.Application.get_default (); search_entry = new Gtk.SearchEntry () { @@ -92,35 +70,61 @@ public class Mail.ConversationList : Gtk.Box { margin_bottom = 3, margin_top = 3 }; - filter_menu_popover_box.add (hide_read_switch); - filter_menu_popover_box.add (hide_unstarred_switch); - filter_menu_popover_box.show_all (); + filter_menu_popover_box.append (hide_read_switch); + filter_menu_popover_box.append (hide_unstarred_switch); - var filter_popover = new Gtk.Popover (null) { + var filter_popover = new Gtk.Popover () { child = filter_menu_popover_box }; filter_button = new Gtk.MenuButton () { - image = new Gtk.Image.from_icon_name ("mail-filter-symbolic", Gtk.IconSize.SMALL_TOOLBAR), + icon_name = "mail-filter-symbolic", popover = filter_popover, tooltip_text = _("Filter Conversations"), valign = Gtk.Align.CENTER }; - search_header = new Hdy.HeaderBar () { - custom_title = search_entry + search_header = new Gtk.HeaderBar () { + title_widget = search_entry, + show_title_buttons = false }; search_header.pack_end (filter_button); - search_header.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + search_header.add_css_class (Granite.STYLE_CLASS_FLAT); + + list_store = new ConversationListStore (); + + var deleted_filter = new Gtk.CustomFilter (deleted_filter_func); + + var filter_model = new Gtk.FilterListModel (list_store, deleted_filter); + + selection_model = new Gtk.SingleSelection (filter_model) { + autoselect = false + }; + + var factory = new Gtk.SignalListItemFactory (); + + list_view = new Gtk.ListView (selection_model, factory) { + show_separators = false + }; + + var event_controller_focus = new Gtk.EventControllerFocus (); + list_view.add_controller (event_controller_focus); - var scrolled_window = new Gtk.ScrolledWindow (null, null) { + context_menu = new Gtk.PopoverMenu.from_model (null) { + position = RIGHT, + has_arrow = false + }; + context_menu.set_parent (list_view); + + var scrolled_window = new Gtk.ScrolledWindow () { hscrollbar_policy = Gtk.PolicyType.NEVER, width_request = 158, - expand = true, - child = list_box + hexpand = true, + vexpand = true, + child = list_view }; - var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic", Gtk.IconSize.SMALL_TOOLBAR) { + var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_REFRESH }; @@ -130,7 +134,7 @@ public class Mail.ConversationList : Gtk.Box { ); var refresh_spinner = new Gtk.Spinner () { - active = true, + spinning = true, halign = Gtk.Align.CENTER, valign = Gtk.Align.CENTER, tooltip_text = _("Fetching new messages…") @@ -145,96 +149,91 @@ public class Mail.ConversationList : Gtk.Box { var conversation_action_bar = new Gtk.ActionBar (); conversation_action_bar.pack_start (refresh_stack); - conversation_action_bar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + conversation_action_bar.add_css_class (Granite.STYLE_CLASS_FLAT); - add (search_header); - add (scrolled_window); - add (conversation_action_bar); + orientation = VERTICAL; + add_css_class (Granite.STYLE_CLASS_VIEW); + + append (search_header); + append (scrolled_window); + append (conversation_action_bar); search_entry.search_changed.connect (() => load_folder.begin (folder_full_name_per_account)); - // Disable delete accelerators when the conversation list box loses keyboard focus, - // restore them when it returns (Replace with EventControllerFocus in GTK4) - list_box.set_focus_child.connect ((widget) => { - if (widget == null) { - application_instance.set_accels_for_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH, - {} - ); - } else { - application_instance.set_accels_for_action ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH, - MainWindow.action_accelerators[MainWindow.ACTION_MOVE_TO_TRASH].to_array () - ); - } + hide_read_switch.toggled.connect (() => load_folder.begin (folder_full_name_per_account)); + + hide_unstarred_switch.toggled.connect (() => load_folder.begin (folder_full_name_per_account)); + + factory.setup.connect ((obj) => { + var list_item = (Gtk.ListItem) obj; + var conversation_list_item = new ConversationListItem (); + conversation_list_item.secondary_click.connect ((x, y) => { + if (!selection_model.is_selected (list_item.get_position ())) { + selection_model.select_item (list_item.get_position (), true); + } + double dest_x; + double dest_y; + conversation_list_item.translate_coordinates (list_view, x, y, out dest_x, out dest_y); + create_context_menu (dest_x, dest_y); + }); + list_item.set_child (conversation_list_item); + }); + + factory.bind.connect ((obj) => { + var list_item = (Gtk.ListItem) obj; + var conversation_list_item = (ConversationListItem) list_item.child; + conversation_list_item.assign ((ConversationItemModel) list_item.get_item ()); }); - list_box.row_activated.connect ((row) => { + selection_model.selection_changed.connect (() => { if (mark_read_timeout_id != 0) { GLib.Source.remove (mark_read_timeout_id); mark_read_timeout_id = 0; } - if (row == null) { - conversation_focused (null); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + + if (!bitset_iter.is_valid ()) { + conversation_selected (null); } else { - conversation_focused (((ConversationItemModel) row).node); + var conversation_item = (ConversationItemModel) selection_model.get_item (current_item_position); - if (((ConversationItemModel) row).unread) { + if (conversation_item.unread) { mark_read_timeout_id = GLib.Timeout.add_seconds (MARK_READ_TIMEOUT_SECONDS, () => { - set_thread_flag (((ConversationItemModel) row).node, Camel.MessageFlags.SEEN); + set_thread_flag (conversation_item.node, Camel.MessageFlags.SEEN); mark_read_timeout_id = 0; return false; }); } - } - }); - - list_box.row_selected.connect ((row) => { - if (row == null) { - conversation_selected (null); - } else { - // We call get_action_group() on the parent window, instead of on `this` directly, due to a - // bug with Gtk.Widget.get_action_group(). See https://gitlab.gnome.org/GNOME/gtk/issues/1396 - var window = (Gtk.ApplicationWindow) get_toplevel (); - weak GLib.ActionMap win_action_map = (GLib.ActionMap) window.get_action_group (MainWindow.ACTION_GROUP_PREFIX); - ((SimpleAction) win_action_map.lookup_action (MainWindow.ACTION_MARK_READ)).set_enabled (((ConversationItemModel) row).unread); - ((SimpleAction) win_action_map.lookup_action (MainWindow.ACTION_MARK_UNREAD)).set_enabled (!((ConversationItemModel) row).unread); - ((SimpleAction) win_action_map.lookup_action (MainWindow.ACTION_MARK_STAR)).set_enabled (!((ConversationItemModel) row).flagged); - ((SimpleAction) win_action_map.lookup_action (MainWindow.ACTION_MARK_UNSTAR)).set_enabled (((ConversationItemModel) row).flagged); - conversation_selected (((ConversationItemModel) row).node); - } - }); - hide_read_switch.toggled.connect (() => load_folder.begin (folder_full_name_per_account)); + var window = (MainWindow) get_root (); + window.get_action (MainWindow.ACTION_MARK_READ).set_enabled (conversation_item.unread); + window.get_action (MainWindow.ACTION_MARK_UNREAD).set_enabled (!conversation_item.unread); + window.get_action (MainWindow.ACTION_MARK_STAR).set_enabled (!conversation_item.flagged); + window.get_action (MainWindow.ACTION_MARK_UNSTAR).set_enabled (conversation_item.flagged); - hide_unstarred_switch.toggled.connect (() => load_folder.begin (folder_full_name_per_account)); - - button_release_event.connect ((e) => { - - if (e.button != Gdk.BUTTON_SECONDARY) { - return Gdk.EVENT_PROPAGATE; - } - - var row = list_box.get_row_at_y ((int)e.y); - - if (list_box.selected_row_widget != row) { - list_box.select_row (row); + conversation_selected (conversation_item.node); } - - return create_context_menu (e, (ConversationListItem)row); }); - key_release_event.connect ((e) => { - - if (e.keyval != Gdk.Key.Menu) { - return Gdk.EVENT_PROPAGATE; - } - - var row = list_box.selected_row_widget; + // Disable delete accelerators when the conversation list box loses keyboard focus, + // restore them when it returns + event_controller_focus.enter.connect (() => { + application_instance.set_accels_for_action ( + MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH, + MainWindow.action_accelerators[MainWindow.ACTION_MOVE_TO_TRASH].to_array () + ); + }); - return create_context_menu (e, (ConversationListItem)row); + event_controller_focus.leave.connect (() => { + application_instance.set_accels_for_action ( + MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH, + {} + ); }); } @@ -261,7 +260,6 @@ public class Mail.ConversationList : Gtk.Box { cancellable.cancel (); } - conversation_focused (null); conversation_selected (null); uint previous_items = list_store.get_n_items (); @@ -273,7 +271,6 @@ public class Mail.ConversationList : Gtk.Box { threads.clear (); list_store.remove_all (); - list_store.items_changed (0, previous_items, 0); cancellable = new GLib.Cancellable (); @@ -324,7 +321,7 @@ public class Mail.ConversationList : Gtk.Box { } } - list_store.items_changed (0, 0, list_store.get_n_items ()); + list_store.items_changed (0, previous_items, list_store.get_n_items ()); } public async void refresh_folder (GLib.Cancellable? cancellable = null) { @@ -358,13 +355,12 @@ public class Mail.ConversationList : Gtk.Box { threads[service_uid] = new Camel.FolderThread (folders[service_uid], search_result_uids, false); - var removed = 0; + var previous_items = list_store.get_n_items (); change_info.get_removed_uids ().foreach ((uid) => { var item = conversations[uid]; if (item != null) { conversations.unset (uid); list_store.remove (item); - removed++; } }); @@ -381,7 +377,6 @@ public class Mail.ConversationList : Gtk.Box { if (item.is_older_than (child)) { conversations.unset (child.message.uid); list_store.remove (item); - removed++; add_conversation_item (folder_info_flags[service_uid], child, threads[service_uid], service_uid); }; } @@ -389,19 +384,18 @@ public class Mail.ConversationList : Gtk.Box { child = child.next; } - list_store.items_changed (0, removed, list_store.get_n_items ()); + list_store.items_changed (0, previous_items, list_store.get_n_items ()); } } } private GenericArray? get_search_result_uids (string service_uid) { - var style_context = filter_button.get_style_context (); if (hide_read_switch.active || hide_unstarred_switch.active) { - if (!style_context.has_class (Granite.STYLE_CLASS_ACCENT)) { - style_context.add_class (Granite.STYLE_CLASS_ACCENT); + if (!filter_button.has_css_class (Granite.STYLE_CLASS_ACCENT)) { + filter_button.add_css_class (Granite.STYLE_CLASS_ACCENT); } - } else if (style_context.has_class (Granite.STYLE_CLASS_ACCENT)) { - style_context.remove_class (Granite.STYLE_CLASS_ACCENT); + } else if (filter_button.has_css_class (Granite.STYLE_CLASS_ACCENT)) { + filter_button.remove_css_class (Granite.STYLE_CLASS_ACCENT); } lock (folders) { @@ -453,60 +447,72 @@ public class Mail.ConversationList : Gtk.Box { list_store.add (item); } - private static bool filter_function (GLib.Object obj) { - if (obj is ConversationItemModel) { - return !((ConversationItemModel)obj).deleted; - } else { - return false; - } - } - - private static int thread_sort_function (ConversationItemModel item1, ConversationItemModel item2) { - return (int)(item2.timestamp - item1.timestamp); + private static bool deleted_filter_func (Object item) { + return !((ConversationItemModel)item).deleted; } public void mark_read_selected_messages () { - var selected_rows = list_box.get_selected_rows (); - foreach (var row in selected_rows) { - (((ConversationItemModel)row).node).message.set_flags (Camel.MessageFlags.SEEN, ~0); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + while (bitset_iter.is_valid ()) { + ((ConversationItemModel)selection_model.get_item (current_item_position)).node.message.set_flags (Camel.MessageFlags.SEEN, ~0); + bitset_iter.next (out current_item_position); } } public void mark_star_selected_messages () { - var selected_rows = list_box.get_selected_rows (); - foreach (var row in selected_rows) { - (((ConversationItemModel)row).node).message.set_flags (Camel.MessageFlags.FLAGGED, ~0); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + while (bitset_iter.is_valid ()) { + ((ConversationItemModel)selection_model.get_item (current_item_position)).node.message.set_flags (Camel.MessageFlags.FLAGGED, ~0); + bitset_iter.next (out current_item_position); } } public void mark_unread_selected_messages () { - var selected_rows = list_box.get_selected_rows (); - foreach (var row in selected_rows) { - (((ConversationItemModel)row).node).message.set_flags (Camel.MessageFlags.SEEN, 0); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + while (bitset_iter.is_valid ()) { + ((ConversationItemModel)selection_model.get_item (current_item_position)).node.message.set_flags (Camel.MessageFlags.SEEN, 0); + bitset_iter.next (out current_item_position); } } public void mark_unstar_selected_messages () { - var selected_rows = list_box.get_selected_rows (); - foreach (var row in selected_rows) { - (((ConversationItemModel)row).node).message.set_flags (Camel.MessageFlags.FLAGGED, 0); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + while (bitset_iter.is_valid ()) { + ((ConversationItemModel)selection_model.get_item (current_item_position)).node.message.set_flags (Camel.MessageFlags.FLAGGED, 0); + bitset_iter.next (out current_item_position); } } public async int archive_selected_messages () { var archive_threads = new Gee.HashMap> (); - var selected_rows = list_box.get_selected_rows (); - int selected_rows_start_index = list_store.get_index_of (selected_rows.to_array ()[0]); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + var selected_items_start_index = current_item_position; - foreach (unowned var selected_row in selected_rows) { - var selected_item_model = (ConversationItemModel) selected_row; + while (bitset_iter.is_valid ()) { + var selected_item_model = (ConversationItemModel)selection_model.get_item (current_item_position); if (archive_threads[selected_item_model.service_uid] == null) { archive_threads[selected_item_model.service_uid] = new Gee.ArrayList (); } archive_threads[selected_item_model.service_uid].add (selected_item_model.node); + bitset_iter.next (out current_item_position); } var archived = 0; @@ -529,8 +535,8 @@ public class Mail.ConversationList : Gtk.Box { } } - list_store.items_changed (0, archived, list_store.get_n_items ()); - list_box.select_row_at_index (selected_rows_start_index); + list_store.items_changed (0, list_store.get_n_items (), list_store.get_n_items ()); + selection_model.select_item (selected_items_start_index, true); return archived; } @@ -538,17 +544,21 @@ public class Mail.ConversationList : Gtk.Box { public int trash_selected_messages () { var trash_threads = new Gee.HashMap> (); - var selected_rows = list_box.get_selected_rows (); - int selected_rows_start_index = list_store.get_index_of (selected_rows.to_array ()[0]); + var selected_items = selection_model.get_selection (); + uint current_item_position; + Gtk.BitsetIter bitset_iter = Gtk.BitsetIter (); + bitset_iter.init_first (selected_items, out current_item_position); + var selected_items_start_index = current_item_position; - foreach (unowned var selected_row in selected_rows) { - var selected_item_model = (ConversationItemModel) selected_row; + while (bitset_iter.is_valid ()) { + var selected_item_model = (ConversationItemModel)selection_model.get_item (current_item_position); if (trash_threads[selected_item_model.service_uid] == null) { trash_threads[selected_item_model.service_uid] = new Gee.ArrayList (); } trash_threads[selected_item_model.service_uid].add (selected_item_model.node); + bitset_iter.next (out current_item_position); } var deleted = 0; @@ -556,8 +566,8 @@ public class Mail.ConversationList : Gtk.Box { deleted += move_handler.delete_threads (folders[service_uid], trash_threads[service_uid]); } - list_store.items_changed (0, 0, list_store.get_n_items ()); - list_box.select_row_at_index (selected_rows_start_index + 1); + list_store.items_changed (0, list_store.get_n_items (), list_store.get_n_items ()); + selection_model.select_item (selected_items_start_index, true); return deleted; } @@ -565,7 +575,7 @@ public class Mail.ConversationList : Gtk.Box { public void undo_move () { move_handler.undo_last_move.begin ((obj, res) => { move_handler.undo_last_move.end (res); - list_store.items_changed (0, 0, list_store.get_n_items ()); + list_store.items_changed (0, list_store.get_n_items (), list_store.get_n_items ()); }); } @@ -573,65 +583,32 @@ public class Mail.ConversationList : Gtk.Box { move_handler.expire_undo (); } - private bool create_context_menu (Gdk.Event e, ConversationListItem row) { - var item = (ConversationItemModel)row.model_item; - - var menu = new Gtk.Menu (); + private void create_context_menu (double x, double y) { + var menu = new Menu (); - var trash_menu_item = new Gtk.MenuItem (); - trash_menu_item.add (new Granite.AccelLabel.from_action_name (_("Move To Trash"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH)); - menu.add (trash_menu_item); + var conversation_item_model = (ConversationItemModel) selection_model.get_selected_item (); - trash_menu_item.activate.connect (() => { - trash_selected_messages (); - }); + menu.append (_("Move To Trash"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH); - if (!item.unread) { - var mark_unread_menu_item = new Gtk.MenuItem (); - mark_unread_menu_item.add (new Granite.AccelLabel.from_action_name (_("Mark As Unread"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNREAD)); - menu.add (mark_unread_menu_item); - - mark_unread_menu_item.activate.connect (() => { - mark_unread_selected_messages (); - }); + if (!conversation_item_model.unread) { + menu.append (_("Mark As Unread"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNREAD); } else { - var mark_read_menu_item = new Gtk.MenuItem (); - mark_read_menu_item.add (new Granite.AccelLabel.from_action_name (_("Mark as Read"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_READ)); - menu.add (mark_read_menu_item); - - mark_read_menu_item.activate.connect (() => { - mark_read_selected_messages (); - }); + menu.append (_("Mark As Read"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_READ); } - if (!item.flagged) { - var mark_starred_menu_item = new Gtk.MenuItem (); - mark_starred_menu_item.add (new Granite.AccelLabel.from_action_name (_("Star"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_STAR)); - menu.add (mark_starred_menu_item); - - mark_starred_menu_item.activate.connect (() => { - mark_star_selected_messages (); - }); + if (!conversation_item_model.flagged) { + menu.append (_("Star"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_STAR); } else { - var mark_unstarred_menu_item = new Gtk.MenuItem (); - mark_unstarred_menu_item.add (new Granite.AccelLabel.from_action_name (_("Unstar"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNSTAR)); - menu.add (mark_unstarred_menu_item); - - mark_unstarred_menu_item.activate.connect (() => { - mark_unstar_selected_messages (); - }); + menu.append (_("Unstar"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNSTAR); } - menu.show_all (); - - if (e.type == Gdk.EventType.BUTTON_RELEASE) { - menu.popup_at_pointer (e); - return Gdk.EVENT_STOP; - } else if (e.type == Gdk.EventType.KEY_RELEASE) { - menu.popup_at_widget (row, Gdk.Gravity.EAST, Gdk.Gravity.CENTER, e); - return Gdk.EVENT_STOP; - } + context_menu.set_menu_model (menu); - return Gdk.EVENT_PROPAGATE; + Gdk.Rectangle pos = Gdk.Rectangle () { + x = (int) x, + y = (int) y + }; + context_menu.set_pointing_to (pos); + context_menu.popup (); } } diff --git a/src/ConversationList/ConversationListItem.vala b/src/ConversationList/ConversationListItem.vala index 7db572596..8322b4fce 100644 --- a/src/ConversationList/ConversationListItem.vala +++ b/src/ConversationList/ConversationListItem.vala @@ -19,7 +19,9 @@ * Authored by: Corentin Noël */ -public class Mail.ConversationListItem : VirtualizingListBoxRow { +public class Mail.ConversationListItem : Gtk.Box { + public signal void secondary_click (double x, double y); + private Gtk.Image status_icon; private Gtk.Label date; private Gtk.Label messages; @@ -29,13 +31,13 @@ public class Mail.ConversationListItem : VirtualizingListBoxRow { private Gtk.Revealer status_revealer; construct { - status_icon = new Gtk.Image.from_icon_name ("mail-unread-symbolic", Gtk.IconSize.MENU); + status_icon = new Gtk.Image.from_icon_name ("mail-unread-symbolic"); status_revealer = new Gtk.Revealer () { child = status_icon }; - var flagged_icon = new Gtk.Image.from_icon_name ("starred-symbolic", Gtk.IconSize.MENU); + var flagged_icon = new Gtk.Image.from_icon_name ("starred-symbolic"); flagged_icon_revealer = new Gtk.Revealer () { child = flagged_icon }; @@ -46,15 +48,13 @@ public class Mail.ConversationListItem : VirtualizingListBoxRow { use_markup = true, xalign = 0 }; - source.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + source.add_css_class (Granite.STYLE_CLASS_H3_LABEL); messages = new Gtk.Label (null) { halign = Gtk.Align.END }; - - weak Gtk.StyleContext messages_style = messages.get_style_context (); - messages_style.add_class (Granite.STYLE_CLASS_BADGE); - messages_style.add_class (Gtk.STYLE_CLASS_FLAT); + messages.add_css_class (Granite.STYLE_CLASS_BADGE); + messages.add_css_class (Granite.STYLE_CLASS_FLAT); topic = new Gtk.Label (null) { hexpand = true, @@ -65,7 +65,7 @@ public class Mail.ConversationListItem : VirtualizingListBoxRow { date = new Gtk.Label (null) { halign = Gtk.Align.END }; - date.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + date.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var grid = new Gtk.Grid () { margin_top = 12, @@ -83,10 +83,17 @@ public class Mail.ConversationListItem : VirtualizingListBoxRow { grid.attach (topic, 1, 1, 2, 1); grid.attach (messages, 3, 1, 1, 1); - get_style_context ().add_class ("conversation-list-item"); - add (grid); + add_css_class ("conversation-list-item"); + append (grid); + + var gesture_click = new Gtk.GestureClick () { + button = Gdk.BUTTON_SECONDARY + }; + add_controller (gesture_click); - show_all (); + gesture_click.released.connect ((n_press, x, y) => { + secondary_click (x, y); + }); } public void assign (ConversationItemModel data) { @@ -105,22 +112,21 @@ public class Mail.ConversationListItem : VirtualizingListBoxRow { uint num_messages = data.num_messages; messages.label = num_messages > 1 ? "%u".printf (num_messages) : null; messages.visible = num_messages > 1; - messages.no_show_all = num_messages <= 1; if (data.unread) { - get_style_context ().add_class ("unread-message"); + add_css_class ("unread-message"); status_icon.icon_name = "mail-unread-symbolic"; status_icon.tooltip_text = _("Unread"); - status_icon.get_style_context ().add_class (Granite.STYLE_CLASS_ACCENT); + status_icon.add_css_class (Granite.STYLE_CLASS_ACCENT); status_revealer.reveal_child = true; - source.get_style_context ().add_class (Granite.STYLE_CLASS_ACCENT); + source.add_css_class (Granite.STYLE_CLASS_ACCENT); } else { - get_style_context ().remove_class ("unread-message"); - status_icon.get_style_context ().remove_class (Granite.STYLE_CLASS_ACCENT); - source.get_style_context ().remove_class (Granite.STYLE_CLASS_ACCENT); + remove_css_class ("unread-message"); + status_icon.remove_css_class (Granite.STYLE_CLASS_ACCENT); + source.remove_css_class (Granite.STYLE_CLASS_ACCENT); if (data.replied_all || data.replied) { status_icon.icon_name = "mail-replied-symbolic"; diff --git a/src/ConversationList/ConversationListStore.vala b/src/ConversationList/ConversationListStore.vala index b83641a30..c077da0e4 100644 --- a/src/ConversationList/ConversationListStore.vala +++ b/src/ConversationList/ConversationListStore.vala @@ -20,94 +20,47 @@ * Authored by: David Hewitt */ -public class Mail.ConversationListStore : VirtualizingListBoxModel { - public delegate bool RowVisibilityFunc (GLib.Object row); +public class Mail.ConversationListStore : ListModel, Object { + /* The items_changed signal is not emitted automatically, the object using this has to emit it manually */ - private GLib.Sequence data = new GLib.Sequence (); - private uint last_position = uint.MAX; - private GLib.SequenceIter? last_iter; - private unowned GLib.CompareDataFunc compare_func; - private unowned RowVisibilityFunc filter_func; + private GLib.List data = new GLib.List (); - public override uint get_n_items () { - return data.get_length (); + public GLib.Type get_item_type () { + return typeof (ConversationItemModel); } - public override GLib.Object? get_item (uint index) { - return get_item_internal (index); - } - - public override GLib.Object? get_item_unfiltered (uint index) { - return get_item_internal (index, true); + public uint get_n_items () { + uint n_items = 0; + data.foreach ((item) => { + n_items++; + }); + return n_items; } - private GLib.Object? get_item_internal (uint index, bool unfiltered = false) { - GLib.SequenceIter? iter = null; - - if (last_position != uint.MAX) { - if (last_position == index + 1) { - iter = last_iter.prev (); - } else if (last_position == index - 1) { - iter = last_iter.next (); - } else if (last_position == index) { - iter = last_iter; - } - } - - if (iter == null) { - iter = data.get_iter_at_pos ((int)index); - } - - last_iter = iter; - last_position = index; - - if (iter.is_end ()) { - return null; - } - - if (filter_func == null) { - return iter.get (); - } else if (filter_func (iter.get ())) { - return iter.get (); - } else if (unfiltered) { - return iter.get (); - } else { - return null; - } + public GLib.Object? get_item (uint index) { + return get_item_internal (index); } - public void add (ConversationItemModel data) { - if (compare_func != null) { - this.data.insert_sorted (data, compare_func); - } else { - this.data.append (data); - } - - last_iter = null; - last_position = uint.MAX; + private ConversationItemModel get_item_internal (uint index) { + return data.nth_data (index); } - public void remove (ConversationItemModel data) { - var iter = this.data.get_iter_at_pos (get_index_of_unfiltered (data)); - iter.remove (); - - last_iter = null; - last_position = uint.MAX; + public void add (ConversationItemModel item) { + /* Adding automatically sorts according to timestamp */ + data.insert_sorted (item, (a, b)=> { + var item1 = (ConversationItemModel) a; + var item2 = (ConversationItemModel) b; + return (int)(item2.timestamp - item1.timestamp); + }); } public void remove_all () { - data.get_begin_iter ().remove_range (data.get_end_iter ()); - unselect_all (); - - last_iter = null; - last_position = uint.MAX; - } - - public void set_sort_func (GLib.CompareDataFunc function) { - this.compare_func = function; + data.foreach ((item) => { + data.remove_all (item); + }); } - public void set_filter_func (RowVisibilityFunc? function) { - filter_func = function; + public void remove (ConversationItemModel item) { + data.remove_all (item); } } diff --git a/src/Dialogs/InsertLinkDialog.vala b/src/Dialogs/InsertLinkDialog.vala index 2a33adbee..e1597eadf 100644 --- a/src/Dialogs/InsertLinkDialog.vala +++ b/src/Dialogs/InsertLinkDialog.vala @@ -59,17 +59,16 @@ public class InsertLinkDialog : Granite.Dialog { grid.attach (url_entry, 1, 0); grid.attach (title_label, 0, 1); grid.attach (title_entry, 1, 1); - grid.show_all (); - get_content_area ().add (grid); + get_content_area ().append (grid); add_button (_("Cancel"), Gtk.ResponseType.CANCEL); var insert_button = add_button (_("Insert Link"), Gtk.ResponseType.APPLY); - insert_button.can_default = true; - insert_button.has_default = true; + // insert_button.can_default = true; + // insert_button.has_default = true; insert_button.sensitive = false; - insert_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + insert_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); deletable = false; modal = true; diff --git a/src/FolderList/AccountItemModel.vala b/src/FolderList/AccountItemModel.vala new file mode 100644 index 000000000..4cbd5c5f6 --- /dev/null +++ b/src/FolderList/AccountItemModel.vala @@ -0,0 +1,98 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2017 elementary LLC. (https://elementary.io) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authored by: Corentin Noël + */ + +public class Mail.AccountItemModel : ItemModel { + public Mail.Backend.Account account { get; construct; } + + private GLib.Cancellable connect_cancellable; + private unowned Camel.OfflineStore offlinestore; + + public AccountItemModel (Mail.Backend.Account account) { + Object (account: account); + } + + construct { + offlinestore = (Camel.OfflineStore) account.service; + + icon_name = "avatar-default"; + name = offlinestore.display_name; + account_uid = offlinestore.uid; + folder_list = new ListStore (typeof (FolderItemModel)); + + connect_cancellable = new GLib.Cancellable (); + + offlinestore.folder_created.connect (load); + offlinestore.folder_deleted.connect (load); + offlinestore.folder_info_stale.connect (load); + offlinestore.folder_renamed.connect (load); + + unowned GLib.NetworkMonitor network_monitor = GLib.NetworkMonitor.get_default (); + network_monitor.network_changed.connect (() =>{ + connect_to_account.begin (); + }); + load.begin (); + } + + ~AccountItemModel () { + connect_cancellable.cancel (); + } + + public async void load () { + try { + var folderinfo = yield offlinestore.get_folder_info (null, Camel.StoreGetFolderInfoFlags.RECURSIVE, GLib.Priority.DEFAULT, connect_cancellable); + if (folderinfo != null) { + show_info (folderinfo); + } + } catch (Error e) { + critical (e.message); + } + + connect_to_account.begin (); + } + + private async void connect_to_account () { + unowned GLib.NetworkMonitor network_monitor = GLib.NetworkMonitor.get_default (); + if (network_monitor.network_available == false) { + return; + } + + try { + yield offlinestore.set_online (true, GLib.Priority.DEFAULT, connect_cancellable); + yield offlinestore.connect (GLib.Priority.DEFAULT, connect_cancellable); + + yield offlinestore.synchronize (false, GLib.Priority.DEFAULT, connect_cancellable); + } catch (Error e) { + critical (e.message); + } + } + + private void show_info (Camel.FolderInfo? _folderinfo) { + folder_list.remove_all (); + + var folderinfo = _folderinfo; + while (folderinfo != null) { + var folder_item = new FolderItemModel (folderinfo, account); + folder_list.append (folder_item); + folderinfo = (Camel.FolderInfo?) folderinfo.next; + } + } +} diff --git a/src/FolderList/FolderItemModel.vala b/src/FolderList/FolderItemModel.vala new file mode 100644 index 000000000..ccc6b023b --- /dev/null +++ b/src/FolderList/FolderItemModel.vala @@ -0,0 +1,92 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2017 elementary LLC. (https://elementary.io) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authored by: Corentin Noël + */ + +public class Mail.FolderItemModel : ItemModel { + public int unread { get; construct; } + + public Mail.Backend.Account account { get; construct; } + public Camel.FolderInfo folder_info { get; construct; } + + public FolderItemModel (Camel.FolderInfo folderinfo, Mail.Backend.Account account) { + Object (account: account, + folder_info: folderinfo + ); + } + + construct { + name = folder_info.display_name; + account_uid = account.service.uid; + + unread = folder_info.unread; + + if (folder_info.child != null) { + if (folder_list == null) { + folder_list = new ListStore (typeof (FolderItemModel)); + } + var current_folder_info = folder_info.child; + while (current_folder_info != null) { + var folder_item = new FolderItemModel (current_folder_info, account); + folder_list.append (folder_item); + + current_folder_info = (Camel.FolderInfo?) current_folder_info.next; + } + } + + var full_folder_info_flags = Utils.get_full_folder_info_flags (account.service, folder_info); + switch (full_folder_info_flags & Camel.FOLDER_TYPE_MASK) { + case Camel.FolderInfoFlags.TYPE_INBOX: + icon_name = "mail-inbox"; + break; + case Camel.FolderInfoFlags.TYPE_OUTBOX: + icon_name = "mail-outbox"; + break; + case Camel.FolderInfoFlags.TYPE_TRASH: + icon_name = folder_info.total == 0 ? "user-trash" : "user-trash-full"; + break; + case Camel.FolderInfoFlags.TYPE_JUNK: + icon_name = "edit-flag"; + break; + case Camel.FolderInfoFlags.TYPE_SENT: + icon_name = "mail-sent"; + break; + case Camel.FolderInfoFlags.TYPE_ARCHIVE: + icon_name = "mail-archive"; + break; + case Camel.FolderInfoFlags.TYPE_DRAFTS: + icon_name = "mail-drafts"; + break; + default: + icon_name = "folder"; + break; + } + } + + public async void refresh () { + var offlinestore = (Camel.Store) account.service; + try { + var folder = yield offlinestore.get_folder (folder_info.full_name, 0, GLib.Priority.DEFAULT, null); + yield folder.refresh_info (GLib.Priority.DEFAULT, null); + } catch (Error e) { + critical (e.message); + } + } +} diff --git a/src/FolderList/FolderList.vala b/src/FolderList/FolderList.vala new file mode 100644 index 000000000..a3cce22dc --- /dev/null +++ b/src/FolderList/FolderList.vala @@ -0,0 +1,209 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2017 elementary LLC. (https://elementary.io) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authored by: Corentin Noël + */ + +public class Mail.FolderList : Gtk.Box { + public signal void folder_selected (Gee.Map folder_full_name_per_account_uid); + + public Gtk.HeaderBar header_bar; + + private static GLib.Settings settings; + + private ListStore root_model; + private Mail.Backend.Session session; + private SessionItemModel session_item; + + private bool already_selected = false; + + static construct { + settings = new GLib.Settings ("io.elementary.mail"); + } + + construct { + var application_instance = (Gtk.Application) GLib.Application.get_default (); + + var compose_button = new Gtk.Button.from_icon_name ("mail-message-new") { + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_COMPOSE_MESSAGE, + halign = Gtk.Align.START + }; + compose_button.tooltip_markup = Granite.markup_accel_tooltip ( + application_instance.get_accels_for_action (compose_button.action_name), + _("Compose new message") + ); + compose_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); + + var window_controls = new Gtk.WindowControls (START); + + header_bar = new Gtk.HeaderBar () { + show_title_buttons = false, + title_widget = new Gtk.Label ("") + }; + header_bar.pack_start (window_controls); + header_bar.pack_end (compose_button); + header_bar.add_css_class (Granite.STYLE_CLASS_FLAT); + + root_model = new ListStore (typeof (ItemModel)); + var tree_list = new Gtk.TreeListModel (root_model, false, false, create_folder_list_func); + var selection_model = new Gtk.SingleSelection (tree_list); + var list_factory = new Gtk.SignalListItemFactory (); + + var folder_list_view = new Gtk.ListView (selection_model, list_factory); + + var scrolled_window = new Gtk.ScrolledWindow () { + child = folder_list_view, + vexpand = true + }; + + orientation = VERTICAL; + width_request = 100; + add_css_class (Granite.STYLE_CLASS_SIDEBAR); + append (header_bar); + append (scrolled_window); + + list_factory.setup.connect ((obj) => { + var list_item = (Gtk.ListItem) obj; + + var tree_expander = new Gtk.TreeExpander () { + child = new FolderListItem () + }; + + list_item.child = tree_expander; + }); + + list_factory.bind.connect ((obj) => { + var list_item = (Gtk.ListItem) obj; + + var expander = (Gtk.TreeExpander) list_item.child; + var list_row = expander.list_row = tree_list.get_row (list_item.position); + + var item = (ItemModel) expander.list_row.item; + + var account_settings = new GLib.Settings.with_path ("io.elementary.mail.accounts", "/io/elementary/mail/accounts/%s/".printf (item.account_uid)); + + if (item is AccountItemModel) { + ((FolderListItem)expander.child).bind (item); + account_settings.bind ("expanded", list_row, "expanded", SettingsBindFlags.DEFAULT | SettingsBindFlags.GET_NO_CHANGES); + } else if (item is FolderItemModel) { + var folder_item = (FolderItemModel)item; + + ((FolderListItem)expander.child).bind (folder_item); + + if (!already_selected) { + string selected_folder_uid, selected_folder_full_name; + settings.get ("selected-folder", "(ss)", out selected_folder_uid, out selected_folder_full_name); + if (folder_item.account_uid == selected_folder_uid && folder_item.folder_info.full_name == selected_folder_full_name) { + selection_model.set_selected (list_item.position); + already_selected = true; + } + } + + if (folder_item.folder_info.full_name in account_settings.get_strv ("expanded-folders")) { + list_row.expanded = true; + } + + list_row.notify["expanded"].connect (() => { + var folders = account_settings.get_strv ("expanded-folders"); + if (list_row.expanded) { + folders += folder_item.folder_info.full_name; + } else { + string[] new_folders = {}; + foreach (var folder in folders) { + if (folder != folder_item.folder_info.full_name) { + new_folders += folder; + } + } + + folders = new_folders; + } + + account_settings.set_strv ("expanded-folders", folders); + }); + } else if (item is SessionItemModel) { + ((FolderListItem)expander.child).bind (item); + // list_row.expanded = true; //@TODO: causes snapshot warning ? + } else if (item is GroupedFolderItemModel) { + var folder_item = (GroupedFolderItemModel)item; + + ((FolderListItem)expander.child).bind (folder_item); + + if (!already_selected) { + string selected_folder_uid, selected_folder_full_name; + settings.get ("selected-folder", "(ss)", out selected_folder_uid, out selected_folder_full_name); + if (folder_item.account_uid == selected_folder_uid && folder_item.full_name == selected_folder_full_name) { + selection_model.set_selected (list_item.position); + already_selected = true; + } + } + } + }); + + session_item = new SessionItemModel (); + + session = Mail.Backend.Session.get_default (); + + session.get_accounts ().foreach ((account) => { + add_account (account); + return true; + }); + + session.account_added.connect (add_account); + + selection_model.selection_changed.connect ((position) => { + var item = ((Gtk.TreeListRow)selection_model.get_selected_item ()).get_item (); + + if (item is FolderItemModel) { + var folder_name_per_account_uid = new Gee.HashMap (); + folder_name_per_account_uid.set (item.account, item.folder_info.full_name); + folder_selected (folder_name_per_account_uid.read_only_view); + + settings.set ("selected-folder", "(ss)", item.account_uid, item.folder_info.full_name); + } else if (item is GroupedFolderItemModel) { + folder_selected (item.get_folder_full_name_per_account ()); + + settings.set ("selected-folder", "(ss)", item.account_uid, item.full_name); + } + }); + } + + public ListModel? create_folder_list_func (Object item) { + if (item is ItemModel) { + return item.folder_list; + } + return null; + } + + private void add_account (Mail.Backend.Account account) { + if (session.get_accounts ().size > 1 && !(root_model.get_item (0) is SessionItemModel)) { + root_model.insert (0, session_item); + } + session_item.add_account (account); + + var account_item = new AccountItemModel (account); + root_model.append (account_item); + } +} + +public class ItemModel : Object { + public string account_uid { get; protected set; } + public string icon_name { get; protected set; } + public string name { get; protected set; } + public ListStore? folder_list { get; protected set; default = null; } +} diff --git a/src/FolderList/FolderListItem.vala b/src/FolderList/FolderListItem.vala new file mode 100644 index 000000000..c5e0f2f04 --- /dev/null +++ b/src/FolderList/FolderListItem.vala @@ -0,0 +1,86 @@ +public class FolderListItem : Gtk.Box { + private Gtk.Image image; + private Gtk.Label label; + private Gtk.Label badge; + + private Mail.FolderItemModel? folder_item = null; + + private const string ACTION_GROUP_PREFIX = "folderlistitem"; + private const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + private const string ACTION_REFRESH = "refresh"; + + construct { + var refresh_action = new SimpleAction (ACTION_REFRESH, null); + refresh_action.activate.connect (on_refresh); + + var actions = new SimpleActionGroup (); + actions.add_action (refresh_action); + insert_action_group (ACTION_GROUP_PREFIX, actions); + + var gesture_secondary_click = new Gtk.GestureClick () { + button = Gdk.BUTTON_SECONDARY + }; + add_controller (gesture_secondary_click); + + var context_menu_model = new Menu (); + context_menu_model.append (_("Refresh"), ACTION_PREFIX + ACTION_REFRESH); + + var menu = new Gtk.PopoverMenu.from_model (context_menu_model) { + has_arrow = false + }; + menu.set_parent (this); + + image = new Gtk.Image (); + + label = new Gtk.Label ("") { + margin_start = 3 + }; + + badge = new Gtk.Label ("") { + halign = END //@TODO: Tbh no idea how to move this to the right without another widget + }; + badge.add_css_class (Granite.STYLE_CLASS_BADGE); + + hexpand = true; + vexpand = true; + orientation = HORIZONTAL; + + append (image); + append (label); + append (badge); + + gesture_secondary_click.pressed.connect ((n_press, x, y) => { + if (folder_item != null) { + var rect = Gdk.Rectangle () { + x = (int) x, + y = (int) y + }; + menu.pointing_to = rect; + menu.popup (); + } + }); + } + + public void bind (ItemModel item_model) { + image.set_from_icon_name (item_model.icon_name); + label.label = item_model.name; + + if (item_model is Mail.FolderItemModel) { + folder_item = (Mail.FolderItemModel)item_model; + badge.label = "%d".printf (item_model.unread); + badge.visible = item_model.unread > 0; + } else if (item_model is Mail.GroupedFolderItemModel) { + badge.label = "%d".printf (item_model.unread); + badge.visible = item_model.unread > 0; + } else { + folder_item = null; + badge.visible = false; + } + } + + private void on_refresh () { + if (folder_item != null) { + folder_item.refresh.begin (); + } + } +} diff --git a/src/FoldersView/GroupedFolderSourceItem.vala b/src/FolderList/GroupedFolderItemModel.vala similarity index 68% rename from src/FoldersView/GroupedFolderSourceItem.vala rename to src/FolderList/GroupedFolderItemModel.vala index 7edf70e11..292f25bec 100644 --- a/src/FoldersView/GroupedFolderSourceItem.vala +++ b/src/FolderList/GroupedFolderItemModel.vala @@ -1,68 +1,69 @@ -/* -* Copyright 2021 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 3 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - -public class Mail.GroupedFolderSourceItem : Mail.SourceList.Item { - public Mail.Backend.Session session { get; construct; } +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*- + * Copyright (c) 2017 elementary LLC. (https://elementary.io) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authored by: Corentin Noël + */ + +public class Mail.GroupedFolderItemModel : ItemModel { + public int unread { get; set; } + public Camel.FolderInfoFlags folder_type { get; construct; } + public string full_name { get; construct; } + private Gee.HashMap account_folderinfo; private GLib.Cancellable connect_cancellable; - private Gee.HashMap account_folderinfo; - public GroupedFolderSourceItem (Mail.Backend.Session session, Camel.FolderInfoFlags folder_type) { - Object (session: session, folder_type: folder_type); + public GroupedFolderItemModel (Camel.FolderInfoFlags folder_type) { + Object (folder_type: folder_type); } construct { - visible = true; + account_uid = Mail.SessionItemModel.SESSION_ACCOUNT_UID; + account_folderinfo = new Gee.HashMap (); + connect_cancellable = new GLib.Cancellable (); - account_folderinfo = new Gee.HashMap (); switch (folder_type & Camel.FOLDER_TYPE_MASK) { case Camel.FolderInfoFlags.TYPE_INBOX: name = _("Inbox"); - icon = new ThemedIcon ("mail-inbox"); + icon_name = "mail-inbox"; + full_name = "inbox"; break; case Camel.FolderInfoFlags.TYPE_ARCHIVE: name = _("Archive"); - icon = new ThemedIcon ("mail-archive"); + icon_name = ("mail-archive"); + full_name = "archive"; break; case Camel.FolderInfoFlags.TYPE_SENT: name = _("Sent"); - icon = new ThemedIcon ("mail-sent"); + icon_name = "mail-sent"; + full_name = "sent"; break; default: name = "%i".printf (folder_type & Camel.FOLDER_TYPE_MASK); - icon = new ThemedIcon ("folder"); + icon_name = "folder"; warning ("Unknown grouped folder type: %s", name); break; } - - session.get_accounts ().foreach ((account) => { - add_account (account); - return true; - }); - - session.account_added.connect (add_account); - session.account_removed.connect (removed_account); } - ~GroupedFolderSourceItem () { + ~GroupedFolderItemModel () { connect_cancellable.cancel (); } @@ -81,7 +82,7 @@ public class Mail.GroupedFolderSourceItem : Mail.SourceList.Item { return folder_full_name_per_account.read_only_view; } - private void add_account (Mail.Backend.Account account) { + public void add_account (Mail.Backend.Account account) { lock (account_folderinfo) { account_folderinfo.set (account, null); } @@ -110,14 +111,7 @@ public class Mail.GroupedFolderSourceItem : Mail.SourceList.Item { update_infos (); } - private void removed_account (Mail.Backend.Account account) { - lock (account_folderinfo) { - account_folderinfo.unset (account); - } - } - private void update_infos () { - badge = null; var total_unread = 0; lock (account_folderinfo) { foreach (var entry in account_folderinfo) { @@ -127,13 +121,11 @@ public class Mail.GroupedFolderSourceItem : Mail.SourceList.Item { total_unread += entry.value.unread; } } - - if (total_unread > 0) { - badge = "%d".printf (total_unread); - } + unread = total_unread; } private string? build_folder_full_name (Backend.Account account) { + var session = Mail.Backend.Session.get_default (); var service_source = session.ref_source (account.service.uid); if (service_source == null || !service_source.has_extension (E.SOURCE_EXTENSION_MAIL_ACCOUNT)) { return null; @@ -165,4 +157,10 @@ public class Mail.GroupedFolderSourceItem : Mail.SourceList.Item { return null; } + + public void remove_account (Mail.Backend.Account account) { + lock (account_folderinfo) { + account_folderinfo.unset (account); + } + } } diff --git a/src/FolderList/SessionItemModel.vala b/src/FolderList/SessionItemModel.vala new file mode 100644 index 000000000..e7f5f6e7d --- /dev/null +++ b/src/FolderList/SessionItemModel.vala @@ -0,0 +1,47 @@ +/* +* Copyright 2021 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +*/ + + public class Mail.SessionItemModel : ItemModel { + public const string SESSION_ACCOUNT_UID = "SESSION ACCOUNT"; + + construct { + name = _("All Mailboxes"); + icon_name = "go-home"; + account_uid = SESSION_ACCOUNT_UID; + + folder_list = new ListStore (typeof (GroupedFolderItemModel)); + folder_list.append (new GroupedFolderItemModel (Camel.FolderInfoFlags.TYPE_INBOX)); + folder_list.append (new GroupedFolderItemModel (Camel.FolderInfoFlags.TYPE_ARCHIVE)); + folder_list.append (new GroupedFolderItemModel (Camel.FolderInfoFlags.TYPE_SENT)); + } + + public void add_account (Mail.Backend.Account account) { + for (int i = 0; folder_list.get_item (i) != null; i++) { + var item = (GroupedFolderItemModel)folder_list.get_item (i); + item.add_account (account); + } + } + + public void remove_account (Mail.Backend.Account account) { + for (int i = 0; folder_list.get_item (i) != null; i++) { + var item = (GroupedFolderItemModel)folder_list.get_item (i); + item.remove_account (account); + } + } +} diff --git a/src/FoldersView/AccountSavedState.vala b/src/FoldersView/AccountSavedState.vala deleted file mode 100644 index 8c85c80fa..000000000 --- a/src/FoldersView/AccountSavedState.vala +++ /dev/null @@ -1,65 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2017 elementary LLC. (https://elementary.io) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Authored by: Corentin Noël - */ - -public class Mail.AccountSavedState : GLib.Object { - public unowned Mail.Backend.Account account { get; construct; } - - private GLib.Settings settings; - private Gee.HashMap items; - - public AccountSavedState (Mail.Backend.Account account) { - Object (account: account); - } - - construct { - settings = new GLib.Settings.with_path ("io.elementary.mail.accounts", "/io/elementary/mail/accounts/%s/".printf (account.service.uid)); - items = new Gee.HashMap (); - } - - public void bind_with_expandable_item (Mail.SourceList.ExpandableItem item) { - if (item is AccountSourceItem) { - settings.bind ("expanded", item, "expanded", SettingsBindFlags.DEFAULT | SettingsBindFlags.GET_NO_CHANGES); - } else if (item is FolderSourceItem) { - var folder_item = (FolderSourceItem) item; - items[folder_item.full_name] = folder_item; - if (folder_item.full_name in settings.get_strv ("expanded-folders")) { - item.expanded = true; - } - - item.notify["expanded"].connect (() => { - var folders = settings.get_strv ("expanded-folders"); - if (item.expanded) { - folders += folder_item.full_name; - } else { - string[] new_folders = {}; - foreach (var folder in folders) { - if (folder != folder_item.full_name) { - new_folders += folder; - } - } - - folders = new_folders; - } - - settings.set_strv ("expanded-folders", folders); - }); - } - } -} diff --git a/src/FoldersView/AccountSourceItem.vala b/src/FoldersView/AccountSourceItem.vala deleted file mode 100644 index 31db1d496..000000000 --- a/src/FoldersView/AccountSourceItem.vala +++ /dev/null @@ -1,161 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2017 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: Corentin Noël - */ - -public class Mail.AccountSourceItem : Mail.SourceList.ExpandableItem { - public Mail.Backend.Account account { get; construct; } - - public signal void loaded (); - - private GLib.Cancellable connect_cancellable; - private Gee.HashMap folder_items; - private AccountSavedState saved_state; - private unowned Camel.OfflineStore offlinestore; - - public AccountSourceItem (Mail.Backend.Account account) { - Object (account: account); - } - - construct { - visible = true; - connect_cancellable = new GLib.Cancellable (); - folder_items = new Gee.HashMap (); - saved_state = new AccountSavedState (account); - saved_state.bind_with_expandable_item (this); - - offlinestore = (Camel.OfflineStore) account.service; - name = offlinestore.display_name; - offlinestore.folder_created.connect (folder_created); - offlinestore.folder_deleted.connect (folder_deleted); - offlinestore.folder_info_stale.connect (reload_folders); - offlinestore.folder_renamed.connect (folder_renamed); - unowned GLib.NetworkMonitor network_monitor = GLib.NetworkMonitor.get_default (); - network_monitor.network_changed.connect (() =>{ - connect_to_account.begin (); - }); - } - - ~AccountSourceItem () { - connect_cancellable.cancel (); - } - - public async void load () { - try { - var folderinfo = yield offlinestore.get_folder_info (null, Camel.StoreGetFolderInfoFlags.RECURSIVE, GLib.Priority.DEFAULT, connect_cancellable); - if (folderinfo != null) { - show_info (folderinfo, this); - } - } catch (Error e) { - critical (e.message); - } - - connect_to_account.begin (); - } - - private async void connect_to_account () { - unowned GLib.NetworkMonitor network_monitor = GLib.NetworkMonitor.get_default (); - if (network_monitor.network_available == false) { - return; - } - - try { - yield offlinestore.set_online (true, GLib.Priority.DEFAULT, connect_cancellable); - yield offlinestore.connect (GLib.Priority.DEFAULT, connect_cancellable); - - yield offlinestore.synchronize (false, GLib.Priority.DEFAULT, connect_cancellable); - } catch (Error e) { - critical (e.message); - } - } - - private void folder_renamed (string old_name, Camel.FolderInfo folder_info) { - var item = folder_items[old_name]; - item.update_infos (folder_info); - } - - private void folder_deleted (Camel.FolderInfo folder_info) { - Mail.FolderSourceItem? item = folder_items[folder_info.full_name]; - if (item != null) { - item.parent.remove (item); - folder_items.unset (folder_info.full_name); - } - } - - private void folder_created (Camel.FolderInfo folder_info) { - if (folder_info.parent == null) { - show_info (folder_info, this); - } else { - unowned Camel.FolderInfo parent_info = (Camel.FolderInfo) folder_info.parent; - var parent_item = folder_items[parent_info.full_name]; - if (parent_item == null) { - // Create the parent, then retry to create the children. - folder_created (parent_info); - folder_created (folder_info); - } else { - show_info (folder_info, parent_item); - } - } - } - - private async void reload_folders () { - var offlinestore = (Camel.OfflineStore) account.service; - foreach (var folder_item in folder_items.values) { - try { - var folder_info = yield offlinestore.get_folder_info (folder_item.full_name, 0, GLib.Priority.DEFAULT, connect_cancellable); - folder_item.update_infos (folder_info); - } catch (Error e) { - // We can cancel the operation - if (!(e is GLib.IOError.CANCELLED)) { - critical (e.message); - } - } - } - } - - private void show_info (Camel.FolderInfo? _folderinfo, Mail.SourceList.ExpandableItem item) { - var folderinfo = _folderinfo; - while (folderinfo != null) { - var folder_item = new FolderSourceItem (account, folderinfo); - saved_state.bind_with_expandable_item (folder_item); - folder_items[folderinfo.full_name] = folder_item; - folder_item.refresh.connect (() => { - refresh_folder.begin (folder_item.full_name); - }); - - if (folderinfo.child != null) { - show_info ((Camel.FolderInfo?) folderinfo.child, folder_item); - } - - item.add (folder_item); - folderinfo = (Camel.FolderInfo?) folderinfo.next; - } - } - - private async void refresh_folder (string folder_name) { - var offlinestore = (Camel.Store) account.service; - try { - var folder = yield offlinestore.get_folder (folder_name, 0, GLib.Priority.DEFAULT, connect_cancellable); - yield folder.refresh_info (GLib.Priority.DEFAULT, connect_cancellable); - } catch (Error e) { - critical (e.message); - } - } -} diff --git a/src/FoldersView/FolderSourceItem.vala b/src/FoldersView/FolderSourceItem.vala deleted file mode 100644 index 59c27e868..000000000 --- a/src/FoldersView/FolderSourceItem.vala +++ /dev/null @@ -1,91 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2017 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: Corentin Noël - */ - -public class Mail.FolderSourceItem : Mail.SourceList.ExpandableItem { - public signal void refresh (); - - public string full_name; - public Backend.Account account { get; construct; } - - private bool can_modify = true; - - public FolderSourceItem (Backend.Account account, Camel.FolderInfo folderinfo) { - Object (account: account); - update_infos (folderinfo); - } - - public override Gtk.Menu? get_context_menu () { - var menu = new Gtk.Menu (); - var refresh_item = new Gtk.MenuItem.with_label (_("Refresh folder")); - menu.add (refresh_item); - menu.show_all (); - - refresh_item.activate.connect (() => refresh ()); - return menu; - } - - public void update_infos (Camel.FolderInfo folderinfo) { - name = folderinfo.display_name; - full_name = folderinfo.full_name; - if (folderinfo.unread > 0) { - badge = "%d".printf (folderinfo.unread); - } - - var full_folder_info_flags = Utils.get_full_folder_info_flags (account.service, folderinfo); - switch (full_folder_info_flags & Camel.FOLDER_TYPE_MASK) { - case Camel.FolderInfoFlags.TYPE_INBOX: - icon = new ThemedIcon ("mail-inbox"); - can_modify = false; - break; - case Camel.FolderInfoFlags.TYPE_OUTBOX: - icon = new ThemedIcon ("mail-outbox"); - can_modify = false; - break; - case Camel.FolderInfoFlags.TYPE_TRASH: - icon = new ThemedIcon (folderinfo.total == 0 ? "user-trash" : "user-trash-full"); - can_modify = false; - badge = null; - break; - case Camel.FolderInfoFlags.TYPE_JUNK: - icon = new ThemedIcon ("edit-flag"); - can_modify = false; - break; - case Camel.FolderInfoFlags.TYPE_SENT: - icon = new ThemedIcon ("mail-sent"); - can_modify = false; - break; - case Camel.FolderInfoFlags.TYPE_ARCHIVE: - icon = new ThemedIcon ("mail-archive"); - can_modify = false; - badge = null; - break; - case Camel.FolderInfoFlags.TYPE_DRAFTS: - icon = new ThemedIcon ("mail-drafts"); - can_modify = false; - break; - default: - icon = new ThemedIcon ("folder"); - can_modify = true; - break; - } - } -} diff --git a/src/FoldersView/FoldersListView.vala b/src/FoldersView/FoldersListView.vala deleted file mode 100644 index fd8eaa45e..000000000 --- a/src/FoldersView/FoldersListView.vala +++ /dev/null @@ -1,143 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2017 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: Corentin Noël - */ - -public class Mail.FoldersListView : Gtk.Grid { - public signal void folder_selected (Gee.Map folder_full_name_per_account); - - public Hdy.HeaderBar header_bar { get; private set; } - - private Mail.SourceList source_list; - private Mail.SessionSourceItem session_source_item; - private static GLib.Settings settings; - - static construct { - settings = new GLib.Settings ("io.elementary.mail"); - } - - construct { - source_list = new Mail.SourceList (); - - var application_instance = (Gtk.Application) GLib.Application.get_default (); - - var compose_button = new Gtk.Button.from_icon_name ("mail-message-new", Gtk.IconSize.LARGE_TOOLBAR) { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_COMPOSE_MESSAGE, - halign = Gtk.Align.START - }; - compose_button.tooltip_markup = Granite.markup_accel_tooltip ( - application_instance.get_accels_for_action (compose_button.action_name), - _("Compose new message") - ); - - header_bar = new Hdy.HeaderBar () { - show_close_button = true - }; - header_bar.pack_end (compose_button); - header_bar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - - var scrolled_window = new Gtk.ScrolledWindow (null, null); - scrolled_window.add (source_list); - - orientation = Gtk.Orientation.VERTICAL; - width_request = 100; - get_style_context ().add_class (Gtk.STYLE_CLASS_SIDEBAR); - add (header_bar); - add (scrolled_window); - - var session = Mail.Backend.Session.get_default (); - - session_source_item = new Mail.SessionSourceItem (session); - source_list.root.add (session_source_item); - - session.get_accounts ().foreach ((account) => { - add_account (account); - return true; - }); - - session.account_added.connect (add_account); - source_list.item_selected.connect ((item) => { - if (item == null) { - return; - } - - if (item is FolderSourceItem) { - unowned FolderSourceItem folder_item = (FolderSourceItem) item; - var folder_name_per_account = new Gee.HashMap (); - folder_name_per_account.set (folder_item.account, folder_item.full_name); - folder_selected (folder_name_per_account.read_only_view); - - settings.set ("selected-folder", "(ss)", folder_item.account.service.uid, folder_item.full_name); - - } else if (item is GroupedFolderSourceItem) { - unowned GroupedFolderSourceItem grouped_folder_item = (GroupedFolderSourceItem) item; - folder_selected (grouped_folder_item.get_folder_full_name_per_account ()); - - settings.set ("selected-folder", "(ss)", "GROUPED", grouped_folder_item.name); - } - }); - } - - private void add_account (Mail.Backend.Account account) { - var account_item = new Mail.AccountSourceItem (account); - source_list.root.add (account_item); - account_item.load.begin ((obj, res) => { - account_item.load.end (res); - - string selected_folder_uid, selected_folder_name; - settings.get ("selected-folder", "(ss)", out selected_folder_uid, out selected_folder_name); - - if (account.service.uid == selected_folder_uid) { - select_saved_folder (account_item, selected_folder_name); - } else if (selected_folder_uid == "GROUPED") { - select_saved_folder (session_source_item, selected_folder_name); - } - }); - } - - private bool select_saved_folder (Mail.SourceList.ExpandableItem item, string selected_folder_name) { - foreach (var child in item.children) { - if (child is FolderSourceItem) { - if (select_saved_folder ((Mail.SourceList.ExpandableItem) child, selected_folder_name)) { - return true; - } - - unowned FolderSourceItem folder_item = (FolderSourceItem) child; - if (folder_item.full_name == selected_folder_name) { - source_list.selected = child; - - var folder_name_per_account = new Gee.HashMap (); - folder_name_per_account.set (folder_item.account, folder_item.full_name); - folder_selected (folder_name_per_account.read_only_view); - return true; - } - } else if (child is GroupedFolderSourceItem) { - unowned GroupedFolderSourceItem grouped_folder_item = (GroupedFolderSourceItem) child; - if (grouped_folder_item.name == selected_folder_name) { - source_list.selected = child; - folder_selected (grouped_folder_item.get_folder_full_name_per_account ()); - return true; - } - } - } - - return false; - } -} diff --git a/src/FoldersView/SessionSourceItem.vala b/src/FoldersView/SessionSourceItem.vala deleted file mode 100644 index 460ff4ba9..000000000 --- a/src/FoldersView/SessionSourceItem.vala +++ /dev/null @@ -1,52 +0,0 @@ -/* -* Copyright 2021 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 3 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - - public class Mail.SessionSourceItem : Mail.SourceList.ExpandableItem { - public Mail.Backend.Session session { get; construct; } - - public SessionSourceItem (Mail.Backend.Session session) { - Object (session: session); - } - - construct { - name = _("All Mailboxes"); - visible = session.get_accounts ().size > 1; - expanded = true; - collapsible = false; - - add (new GroupedFolderSourceItem (session, Camel.FolderInfoFlags.TYPE_INBOX)); - add (new GroupedFolderSourceItem (session, Camel.FolderInfoFlags.TYPE_ARCHIVE)); - add (new GroupedFolderSourceItem (session, Camel.FolderInfoFlags.TYPE_SENT)); - - session.account_added.connect (added_account); - session.account_removed.connect (removed_account); - } - - private void added_account (Mail.Backend.Account account) { - if (session.get_accounts ().size > 1) { - visible = true; - } - } - - private void removed_account (Mail.Backend.Account account) { - if (session.get_accounts ().size < 2) { - visible = false; - } - } -} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index bdb17f59d..8007e4733 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -18,17 +18,14 @@ * Authored by: Corentin Noël */ -public class Mail.MainWindow : Hdy.ApplicationWindow { +public class Mail.MainWindow : Adw.ApplicationWindow { private Gtk.Paned paned_end; private Gtk.Paned paned_start; - private FoldersListView folders_list_view; - private Gtk.Overlay view_overlay; private ConversationList conversation_list; + private Granite.Toast toast; private MessageList message_list; - private uint configure_id; - public bool is_session_started { get; private set; default = false; } public signal void session_started (); @@ -105,25 +102,32 @@ public class Mail.MainWindow : Hdy.ApplicationWindow { ); } - folders_list_view = new FoldersListView (); + var folder_list = new FolderList (); + conversation_list = new ConversationList (); message_list = new MessageList (); - view_overlay = new Gtk.Overlay () { - expand = true + var view_overlay = new Gtk.Overlay () { + vexpand = true, + hexpand = true, + child = message_list }; - view_overlay.add (message_list); - var message_overlay = new Granite.Widgets.OverlayBar (view_overlay); - message_overlay.no_show_all = true; + toast = new Granite.Toast (""); + toast.set_default_action (_("Undo")); + view_overlay.add_overlay (toast); + + toast.default_action.connect (() => { + conversation_list.undo_move (); + }); + + var message_overlay = new Granite.OverlayBar (view_overlay) { + visible = false + }; message_list.hovering_over_link.connect ((label, url) => { -#if HAS_SOUP_3 var hover_url = url != null ? GLib.Uri.unescape_string (url) : null; -#else - var hover_url = url != null ? Soup.URI.decode (url) : null; -#endif if (hover_url == null) { message_overlay.hide (); @@ -133,13 +137,21 @@ public class Mail.MainWindow : Hdy.ApplicationWindow { } }); - paned_start = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); - paned_start.pack1 (folders_list_view, false, false); - paned_start.pack2 (conversation_list, true, false); + paned_start = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) { + shrink_start_child = false, + shrink_end_child = false, + wide_handle = true + }; + paned_start.set_start_child (folder_list); + paned_start.set_end_child (conversation_list); - paned_end = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); - paned_end.pack1 (paned_start, false, false); - paned_end.pack2 (view_overlay, true, false); + paned_end = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) { + shrink_start_child = false, + shrink_end_child = false, + wide_handle = true + }; + paned_end.set_start_child (paned_start); + paned_end.set_end_child (view_overlay); var welcome_view = new Mail.WelcomeView (); @@ -147,25 +159,22 @@ public class Mail.MainWindow : Hdy.ApplicationWindow { placeholder_stack.add_named (paned_end, "mail"); placeholder_stack.add_named (welcome_view, "welcome"); - add (placeholder_stack); - - var header_group = new Hdy.HeaderGroup (); - header_group.add_header_bar (folders_list_view.header_bar); - header_group.add_header_bar (conversation_list.search_header); - header_group.add_header_bar (message_list.headerbar); + set_content (placeholder_stack); var size_group = new Gtk.SizeGroup (Gtk.SizeGroupMode.VERTICAL); - size_group.add_widget (folders_list_view.header_bar); + size_group.add_widget (folder_list.header_bar); size_group.add_widget (conversation_list.search_header); size_group.add_widget (message_list.headerbar); var settings = new GLib.Settings ("io.elementary.mail"); + settings.bind ("window-width", this, "default-width", SettingsBindFlags.DEFAULT); + settings.bind ("window-height", this, "default-height", SettingsBindFlags.DEFAULT); + settings.bind ("window-maximized", this, "maximized", SettingsBindFlags.DEFAULT); + settings.bind ("paned-start-position", paned_start, "position", SettingsBindFlags.DEFAULT); settings.bind ("paned-end-position", paned_end, "position", SettingsBindFlags.DEFAULT); - destroy.connect (() => destroy ()); - - folders_list_view.folder_selected.connect (conversation_list.load_folder); + folder_list.folder_selected.connect (conversation_list.load_folder); conversation_list.conversation_selected.connect (message_list.set_conversation); @@ -205,18 +214,26 @@ public class Mail.MainWindow : Hdy.ApplicationWindow { private void on_mark_read () { conversation_list.mark_read_selected_messages (); + get_action (ACTION_MARK_READ).set_enabled (false); + get_action (ACTION_MARK_UNREAD).set_enabled (true); } private void on_mark_star () { conversation_list.mark_star_selected_messages (); + get_action (ACTION_MARK_STAR).set_enabled (false); + get_action (ACTION_MARK_UNSTAR).set_enabled (true); } private void on_mark_unread () { conversation_list.mark_unread_selected_messages (); + get_action (ACTION_MARK_UNREAD).set_enabled (false); + get_action (ACTION_MARK_READ).set_enabled (true); } private void on_mark_unstar () { conversation_list.mark_unstar_selected_messages (); + get_action (ACTION_MARK_UNSTAR).set_enabled (false); + get_action (ACTION_MARK_STAR).set_enabled (true); } private void action_compose (SimpleAction action, Variant? parameter) { @@ -247,74 +264,22 @@ public class Mail.MainWindow : Hdy.ApplicationWindow { private void on_move_to_trash () { var result = conversation_list.trash_selected_messages (); if (result > 0) { - send_move_toast (ngettext ("Message Deleted", "Messages Deleted", result)); - } - } - - private void send_move_toast (string message) { - foreach (weak Gtk.Widget child in view_overlay.get_children ()) { - if (child is Granite.Widgets.Toast) { - child.destroy (); - } + toast.title = ngettext ("Message Deleted", "Messages Deleted", result); + toast.send_notification (); } - - var toast = new Granite.Widgets.Toast (message); - toast.set_default_action (_("Undo")); - toast.show_all (); - - toast.default_action.connect (() => { - conversation_list.undo_move (); - }); - - toast.notify["child-revealed"].connect (() => { - if (!toast.child_revealed) { - conversation_list.undo_expired (); - } - }); - - view_overlay.add_overlay (toast); - toast.send_notification (); } private void on_fullscreen () { - if (Gdk.WindowState.FULLSCREEN in get_window ().get_state ()) { - message_list.headerbar.show_close_button = true; + if (is_fullscreen ()) { + message_list.window_controls.visible = true; unfullscreen (); } else { - message_list.headerbar.show_close_button = false; + message_list.window_controls.visible = false; fullscreen (); } } - private SimpleAction? get_action (string name) { - return lookup_action (name) as SimpleAction; - } - - public override bool configure_event (Gdk.EventConfigure event) { - if (configure_id != 0) { - GLib.Source.remove (configure_id); - } - - configure_id = Timeout.add (100, () => { - configure_id = 0; - - if (is_maximized) { - Mail.Application.settings.set_boolean ("window-maximized", true); - } else { - Mail.Application.settings.set_boolean ("window-maximized", false); - - Gdk.Rectangle rect; - get_allocation (out rect); - Mail.Application.settings.set ("window-size", "(ii)", rect.width, rect.height); - - int root_x, root_y; - get_position (out root_x, out root_y); - Mail.Application.settings.set ("window-position", "(ii)", root_x, root_y); - } - - return false; - }); - - return base.configure_event (event); + public SimpleAction? get_action (string name) { + return (SimpleAction) lookup_action (name) ; } } diff --git a/src/MessageList/AttachmentButton.vala b/src/MessageList/AttachmentButton.vala index 539cdd8f0..c6a8cdf99 100644 --- a/src/MessageList/AttachmentButton.vala +++ b/src/MessageList/AttachmentButton.vala @@ -53,25 +53,19 @@ public class AttachmentButton : Gtk.FlowBoxChild { actions.add_action (save_as_action); insert_action_group (ACTION_GROUP_PREFIX, actions); + var gesture_secondary_click = new Gtk.GestureClick () { + button = Gdk.BUTTON_SECONDARY + }; + add_controller (gesture_secondary_click); + var context_menu_model = new Menu (); context_menu_model.append (_("Open"), ACTION_PREFIX + ACTION_OPEN); context_menu_model.append (_("Save As…"), ACTION_PREFIX + ACTION_SAVE_AS); - var menu = new Gtk.Menu.from_model (context_menu_model); - - var event_box = new Gtk.EventBox (); - event_box.events |= Gdk.EventMask.BUTTON_PRESS_MASK; - event_box.button_press_event.connect ((event) => { - if (event.button == Gdk.BUTTON_SECONDARY) { - menu.attach_widget = this; - menu.show_all (); - menu.popup_at_pointer (event); - } else { - activate (); - } - - return true; - }); + var menu = new Gtk.PopoverMenu.from_model (context_menu_model) { + has_arrow = false + }; + menu.set_parent (this); var grid = new Gtk.Grid () { margin_top = 6, @@ -85,7 +79,7 @@ public class AttachmentButton : Gtk.FlowBoxChild { var glib_type = GLib.ContentType.from_mime_type (mime_type); var content_icon = GLib.ContentType.get_icon (glib_type); - preview_image = new Gtk.Image.from_gicon (content_icon, Gtk.IconSize.DND) { + preview_image = new Gtk.Image.from_gicon (content_icon) { valign = Gtk.Align.CENTER }; @@ -96,7 +90,7 @@ public class AttachmentButton : Gtk.FlowBoxChild { size_label = new Gtk.Label (null) { xalign = 0 }; - size_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + size_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); new Thread (null, () => { string? size_text = null; @@ -112,7 +106,7 @@ public class AttachmentButton : Gtk.FlowBoxChild { size_label.label = size_text; } else { size_label.label = _("Unknown"); - size_label.get_style_context ().add_class (Gtk.STYLE_CLASS_ERROR); + size_label.add_css_class (Granite.STYLE_CLASS_ERROR); } return GLib.Source.REMOVE; @@ -124,13 +118,20 @@ public class AttachmentButton : Gtk.FlowBoxChild { grid.attach (preview_image, 0, 0, 1, 2); grid.attach (name_label, 1, 0, 1, 1); grid.attach (size_label, 1, 1, 1, 1); - event_box.add (grid); - add (event_box); - show_all (); + set_child (grid); + + gesture_secondary_click.pressed.connect ((n_press, x, y) => { + var rect = Gdk.Rectangle () { + x = (int) x, + y = (int) y + }; + menu.pointing_to = rect; + menu.popup (); + }); } private void on_save_as () { - Gtk.Window? parent_window = get_toplevel () as Gtk.Window; + Gtk.Window? parent_window = (Gtk.Window) get_root (); var chooser = new Gtk.FileChooserNative ( null, parent_window, @@ -140,13 +141,15 @@ public class AttachmentButton : Gtk.FlowBoxChild { ); chooser.set_current_name (mime_part.get_filename ()); - chooser.do_overwrite_confirmation = true; - if (chooser.run () == Gtk.ResponseType.ACCEPT) { - write_to_file.begin (chooser.get_file ()); - } + chooser.response.connect ((response) => { + if (response == Gtk.ResponseType.ACCEPT) { + write_to_file.begin (chooser.get_file ()); + } + chooser.destroy (); + }); - chooser.destroy (); + chooser.show (); } private async void write_to_file (GLib.File file) { diff --git a/src/MessageList/GravatarIcon.vala b/src/MessageList/GravatarIcon.vala deleted file mode 100644 index f5d01d8fe..000000000 --- a/src/MessageList/GravatarIcon.vala +++ /dev/null @@ -1,61 +0,0 @@ -/* -* Copyright 2021 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 3 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - -public class GravatarIcon: Object, Icon, LoadableIcon { - - public string address { get; construct; } - public int scale { get; construct; } - - public GravatarIcon (string address, int scale) { - Object (address: address, scale: scale); - } - - public bool equal (Icon? icon) { - var gravatar_icon = (GravatarIcon?) icon; - if (gravatar_icon == null) { - return false; - } - return address == gravatar_icon.address && scale == gravatar_icon.scale; - } - - public uint hash () { - return "%s-@%i".printf (address, scale).hash (); - } - - public InputStream load (int size, out string? type, Cancellable? cancellable = null) throws Error { - var uri = "https://secure.gravatar.com/avatar/%s?d=404&s=%d".printf ( - Checksum.compute_for_string (ChecksumType.MD5, address.strip ().down ()), - size * scale - ); - type = null; - var server_file = File.new_for_uri (uri); - var path = Path.build_filename (Environment.get_tmp_dir (), server_file.get_basename ()); - var local_file = File.new_for_path (path); - - if (!local_file.query_exists (cancellable)) { - server_file.copy (local_file, FileCopyFlags.OVERWRITE, cancellable, null); - } - - return local_file.read (); - } - - public async InputStream load_async (int size, Cancellable? cancellable = null, out string? type = null) throws Error { - return load (size, out type, cancellable); - } -} diff --git a/src/MessageList/MessageList.vala b/src/MessageList/MessageList.vala index 7432d6383..a3c96f21d 100644 --- a/src/MessageList/MessageList.vala +++ b/src/MessageList/MessageList.vala @@ -7,22 +7,25 @@ public class Mail.MessageList : Gtk.Box { public signal void hovering_over_link (string? label, string? uri); - public Hdy.HeaderBar headerbar { get; private set; } + public Gtk.WindowControls window_controls { get; set; } + public Gtk.HeaderBar headerbar { get; private set; } + private Gtk.PopoverMenu mark_popover; private Gtk.ListBox list_box; private Gtk.ScrolledWindow scrolled_window; private Gee.HashMap messages; construct { - get_style_context ().add_class (Gtk.STYLE_CLASS_BACKGROUND); + add_css_class (Granite.STYLE_CLASS_BACKGROUND); var application_instance = (Gtk.Application) GLib.Application.get_default (); var load_images_menuitem = new Granite.SwitchModelButton (_("Always Show Remote Images")); - var account_settings_menuitem = new Gtk.ModelButton () { - text = _("Account Settings…") + var account_settings_menuitem = new Gtk.Button () { + label = _("Account Settings…") }; + account_settings_menuitem.add_css_class (Granite.STYLE_CLASS_MENUITEM); var app_menu_separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL) { margin_bottom = 3, @@ -33,21 +36,21 @@ public class Mail.MessageList : Gtk.Box { margin_bottom = 3, margin_top = 3 }; - app_menu_box.add (load_images_menuitem); - app_menu_box.add (app_menu_separator); - app_menu_box.add (account_settings_menuitem); - app_menu_box.show_all (); + app_menu_box.append (load_images_menuitem); + app_menu_box.append (app_menu_separator); + app_menu_box.append (account_settings_menuitem); - var app_menu_popover = new Gtk.Popover (null); - app_menu_popover.add (app_menu_box); + var app_menu_popover = new Gtk.Popover (); + app_menu_popover.set_child (app_menu_box); var app_menu = new Gtk.MenuButton () { - image = new Gtk.Image.from_icon_name ("open-menu", Gtk.IconSize.LARGE_TOOLBAR), + icon_name = "open-menu", popover = app_menu_popover, tooltip_text = _("Menu") }; + app_menu.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - var reply_button = new Gtk.Button.from_icon_name ("mail-reply-sender", Gtk.IconSize.LARGE_TOOLBAR) { + var reply_button = new Gtk.Button.from_icon_name ("mail-reply-sender") { //Large toolbar action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_REPLY, action_target = "" }; @@ -55,8 +58,9 @@ public class Mail.MessageList : Gtk.Box { application_instance.get_accels_for_action (reply_button.action_name + "::"), _("Reply") ); + reply_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - var reply_all_button = new Gtk.Button.from_icon_name ("mail-reply-all", Gtk.IconSize.LARGE_TOOLBAR) { + var reply_all_button = new Gtk.Button.from_icon_name ("mail-reply-all") { //Large toolbar action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_REPLY_ALL, action_target = "" }; @@ -64,8 +68,9 @@ public class Mail.MessageList : Gtk.Box { application_instance.get_accels_for_action (reply_all_button.action_name + "::"), _("Reply All") ); + reply_all_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - var forward_button = new Gtk.Button.from_icon_name ("mail-forward", Gtk.IconSize.LARGE_TOOLBAR) { + var forward_button = new Gtk.Button.from_icon_name ("mail-forward") { //Large toolbar action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_FORWARD, action_target = "" }; @@ -73,65 +78,43 @@ public class Mail.MessageList : Gtk.Box { application_instance.get_accels_for_action (forward_button.action_name + "::"), _("Forward") ); + forward_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - var mark_unread_item = new Gtk.MenuItem () { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNREAD - }; - mark_unread_item.bind_property ("sensitive", mark_unread_item, "visible"); - mark_unread_item.add (new Granite.AccelLabel.from_action_name (_("Mark as Unread"), mark_unread_item.action_name)); - - var mark_read_item = new Gtk.MenuItem () { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_READ - }; - mark_read_item.bind_property ("sensitive", mark_read_item, "visible"); - mark_read_item.add (new Granite.AccelLabel.from_action_name (_("Mark as Read"), mark_read_item.action_name)); - - var mark_star_item = new Gtk.MenuItem () { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_STAR - }; - mark_star_item.bind_property ("sensitive", mark_star_item, "visible"); - mark_star_item.add (new Granite.AccelLabel.from_action_name (_("Star"), mark_star_item.action_name)); - - var mark_unstar_item = new Gtk.MenuItem () { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNSTAR - }; - mark_unstar_item.bind_property ("sensitive", mark_unstar_item, "visible"); - mark_unstar_item.add (new Granite.AccelLabel.from_action_name (_("Unstar"), mark_unstar_item.action_name)); - - var mark_menu = new Gtk.Menu (); - mark_menu.add (mark_unread_item); - mark_menu.add (mark_read_item); - mark_menu.add (mark_star_item); - mark_menu.add (mark_unstar_item); - mark_menu.show_all (); - - var mark_button = new Gtk.MenuButton () { + var mark_button = new Gtk.Button () { action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK, - image = new Gtk.Image.from_icon_name ("edit-mark", Gtk.IconSize.LARGE_TOOLBAR), - popup = mark_menu, + icon_name = "edit-mark", tooltip_text = _("Mark Conversation") }; + mark_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); + + mark_popover = new Gtk.PopoverMenu.from_model (null); + mark_popover.set_parent (mark_button); - var archive_button = new Gtk.Button.from_icon_name ("mail-archive", Gtk.IconSize.LARGE_TOOLBAR) { + var archive_button = new Gtk.Button.from_icon_name ("mail-archive") { //Large toolbar action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_ARCHIVE }; archive_button.tooltip_markup = Granite.markup_accel_tooltip ( application_instance.get_accels_for_action (archive_button.action_name), _("Move conversations to archive") ); + archive_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - var trash_button = new Gtk.Button.from_icon_name ("edit-delete", Gtk.IconSize.LARGE_TOOLBAR) { + var trash_button = new Gtk.Button.from_icon_name ("edit-delete") { action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_MOVE_TO_TRASH }; trash_button.tooltip_markup = Granite.markup_accel_tooltip ( application_instance.get_accels_for_action (trash_button.action_name), _("Move conversations to Trash") ); + trash_button.add_css_class (Granite.STYLE_CLASS_LARGE_ICONS); - headerbar = new Hdy.HeaderBar () { - show_close_button = true + window_controls = new Gtk.WindowControls (END); + + headerbar = new Gtk.HeaderBar () { + show_title_buttons = false, + title_widget = new Gtk.Label ("") }; - headerbar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + headerbar.add_css_class (Granite.STYLE_CLASS_FLAT); headerbar.pack_start (reply_button); headerbar.pack_start (reply_all_button); headerbar.pack_start (forward_button); @@ -139,51 +122,71 @@ public class Mail.MessageList : Gtk.Box { headerbar.pack_start (mark_button); headerbar.pack_start (archive_button); headerbar.pack_start (trash_button); + headerbar.pack_end (window_controls); headerbar.pack_end (app_menu); - var settings = new GLib.Settings ("io.elementary.mail"); - settings.bind ("always-load-remote-images", load_images_menuitem, "active", SettingsBindFlags.DEFAULT); - - account_settings_menuitem.clicked.connect (() => { - try { - AppInfo.launch_default_for_uri ("settings://accounts/online", null); - } catch (Error e) { - warning ("Failed to open account settings: %s", e.message); - } - }); - var placeholder = new Gtk.Label (_("No Message Selected")) { visible = true }; - - var placeholder_style_context = placeholder.get_style_context (); - placeholder_style_context.add_class (Granite.STYLE_CLASS_H2_LABEL); - placeholder_style_context.add_class (Gtk.STYLE_CLASS_DIM_LABEL); + placeholder.add_css_class (Granite.STYLE_CLASS_H2_LABEL); + placeholder.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); list_box = new Gtk.ListBox () { hexpand = true, vexpand = true, selection_mode = NONE }; - - list_box.get_style_context ().add_class (Gtk.STYLE_CLASS_BACKGROUND); + list_box.add_css_class (Granite.STYLE_CLASS_BACKGROUND); list_box.set_placeholder (placeholder); list_box.set_sort_func (message_sort_function); - scrolled_window = new Gtk.ScrolledWindow (null, null) { + scrolled_window = new Gtk.ScrolledWindow () { hscrollbar_policy = NEVER }; - scrolled_window.add (list_box); + scrolled_window.set_child (list_box); - // Prevent the focus of the webview causing the ScrolledWindow to scroll + // Prevent the focus of the webview causing the ScrolledWindow to scroll. @TODO: correct replacement? var scrolled_child = scrolled_window.get_child (); - if (scrolled_child is Gtk.Container) { - ((Gtk.Container) scrolled_child).set_focus_vadjustment (new Gtk.Adjustment (0, 0, 0, 0, 0, 0)); + if (scrolled_child is Gtk.Viewport) { + ((Gtk.Viewport) scrolled_child).scroll_to_focus = false; } orientation = VERTICAL; - add (headerbar); - add (scrolled_window); + append (headerbar); + append (scrolled_window); + + var settings = new GLib.Settings ("io.elementary.mail"); + settings.bind ("always-load-remote-images", load_images_menuitem, "active", SettingsBindFlags.DEFAULT); + + account_settings_menuitem.clicked.connect (() => { + try { + AppInfo.launch_default_for_uri ("settings://accounts/online", null); + } catch (Error e) { + warning ("Failed to open account settings: %s", e.message); + } + }); + + mark_button.clicked.connect (create_context_menu); + } + + public void create_context_menu () { + unowned var main_window = (MainWindow) get_root (); + var mark_menu = new Menu (); + + if (main_window.get_action (MainWindow.ACTION_MARK_UNREAD).enabled) { + mark_menu.append (_("Mark as Unread"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNREAD); + } else { + mark_menu.append (_("Mark as Read"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_READ); + } + + if (main_window.get_action (MainWindow.ACTION_MARK_STAR).enabled) { + mark_menu.append (_("Star"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_STAR); + } else { + mark_menu.append (_("Unstar"), MainWindow.ACTION_PREFIX + MainWindow.ACTION_MARK_UNSTAR); + } + + mark_popover.set_menu_model (mark_menu); + mark_popover.popup (); } public void set_conversation (Camel.FolderThreadNode? node) { @@ -195,9 +198,12 @@ public class Mail.MessageList : Gtk.Box { can_reply (false); can_move_thread (false); - list_box.get_children ().foreach ((child) => { - child.destroy (); - }); + var current_child = list_box.get_row_at_index (0); + for (int i = 0; current_child != null; i++) { + list_box.remove (current_child); + current_child = list_box.get_row_at_index (i); + } + messages = new Gee.HashMap (null, null); if (node == null) { @@ -211,24 +217,20 @@ public class Mail.MessageList : Gtk.Box { can_move_thread (true); var item = new MessageListItem (node.message); - list_box.add (item); + list_box.append (item); messages.set (node.message.uid, item); if (node.child != null) { go_down ((Camel.FolderThreadNode?) node.child); } - var children = list_box.get_children (); - var num_children = children.length (); - if (num_children > 0) { - var child = list_box.get_row_at_index ((int) num_children - 1); - if (child != null && child is MessageListItem) { - var list_item = (MessageListItem) child; - list_item.expanded = true; + var child = list_box.get_last_child ().get_prev_sibling (); //The last child is the placeholder + if (child != null && child is MessageListItem) { + var list_item = (MessageListItem) child; + list_item.expanded = true; + can_reply (list_item.loaded); + list_item.notify["loaded"].connect (() => { can_reply (list_item.loaded); - list_item.notify["loaded"].connect (() => { - can_reply (list_item.loaded); - }); - } + }); } if (node.message != null && Camel.MessageFlags.DRAFT in (int) node.message.flags) { @@ -240,7 +242,7 @@ public class Mail.MessageList : Gtk.Box { unowned Camel.FolderThreadNode? current_node = node; while (current_node != null) { var item = new MessageListItem (current_node.message); - list_box.add (item); + list_box.append (item); messages.set (current_node.message.uid, item); if (current_node.next != null) { go_down ((Camel.FolderThreadNode?) current_node.next); @@ -252,7 +254,7 @@ public class Mail.MessageList : Gtk.Box { public async void compose (Composer.Type type, Variant uid) { /* Can't open a new composer if thread is empty*/ - var last_child = list_box.get_row_at_index ((int) list_box.get_children ().length () - 1); + var last_child = list_box.get_last_child ().get_prev_sibling (); //The last child is the placeholder if (last_child == null) { return; } @@ -287,17 +289,17 @@ public class Mail.MessageList : Gtk.Box { } private void can_reply (bool enabled) { - unowned var main_window = (Gtk.ApplicationWindow) get_toplevel (); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_FORWARD)).set_enabled (enabled); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_REPLY_ALL)).set_enabled (enabled); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_REPLY)).set_enabled (enabled); + unowned var main_window = (MainWindow) get_root (); + main_window.get_action (MainWindow.ACTION_FORWARD).set_enabled (enabled); + main_window.get_action (MainWindow.ACTION_REPLY_ALL).set_enabled (enabled); + main_window.get_action (MainWindow.ACTION_REPLY).set_enabled (enabled); } private void can_move_thread (bool enabled) { - unowned var main_window = (Gtk.ApplicationWindow) get_toplevel (); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_ARCHIVE)).set_enabled (enabled); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_MARK)).set_enabled (enabled); - ((SimpleAction) main_window.lookup_action (MainWindow.ACTION_MOVE_TO_TRASH)).set_enabled (enabled); + unowned var main_window = (MainWindow) get_root (); + main_window.get_action (MainWindow.ACTION_ARCHIVE).set_enabled (enabled); + main_window.get_action (MainWindow.ACTION_MARK).set_enabled (enabled); + main_window.get_action (MainWindow.ACTION_MOVE_TO_TRASH).set_enabled (enabled); } private static int message_sort_function (Gtk.ListBoxRow item1, Gtk.ListBoxRow item2) { diff --git a/src/MessageList/MessageListItem.vala b/src/MessageList/MessageListItem.vala index c944acef0..c2a3c2954 100644 --- a/src/MessageList/MessageListItem.vala +++ b/src/MessageList/MessageListItem.vala @@ -29,8 +29,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { private Gtk.InfoBar blocked_images_infobar; private Gtk.Revealer secondary_revealer; private Gtk.Stack header_stack; - private Gtk.StyleContext style_context; - private Hdy.Avatar avatar; + private Adw.Avatar avatar; private Gtk.FlowBox attachment_bar = null; private string message_content; @@ -52,9 +51,9 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { get_message.begin (); message_loaded = true; } - style_context.remove_class ("collapsed"); + remove_css_class ("collapsed"); } else { - style_context.add_class ("collapsed"); + add_css_class ("collapsed"); } } } @@ -79,8 +78,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { construct { loading_cancellable = new GLib.Cancellable (); - style_context = get_style_context (); - style_context.add_class (Granite.STYLE_CLASS_CARD); + add_css_class (Granite.STYLE_CLASS_CARD); unowned string? parsed_address; unowned string? parsed_name; @@ -93,7 +91,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { parsed_name = parsed_address; } - avatar = new Hdy.Avatar (48, parsed_name, true) { + avatar = new Adw.Avatar (48, parsed_name, true) { valign = Gtk.Align.START }; @@ -101,19 +99,19 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { halign = END, valign = START }; - from_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + from_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var to_label = new Gtk.Label (_("To:")) { halign = END, valign = START }; - to_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + to_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var subject_label = new Gtk.Label (_("Subject:")) { halign = END, valign = START }; - subject_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + subject_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var from_val_label = new Gtk.Label (message_info.from) { wrap = true, @@ -147,7 +145,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { halign = END, valign = START }; - cc_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + cc_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var cc_val_label = new Gtk.Label (cc_info) { wrap = true, @@ -167,12 +165,12 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { small_fields_grid.attach (small_from_label, 0, 0, 1, 1); header_stack = new Gtk.Stack () { - homogeneous = false, + hhomogeneous = false, + vhomogeneous = false, transition_type = CROSSFADE }; header_stack.add_named (fields_grid, "large"); header_stack.add_named (small_fields_grid, "small"); - header_stack.show_all (); var relevant_timestamp = message_info.date_received; if (relevant_timestamp == 0) { @@ -185,10 +183,9 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { ///TRANSLATORS: The first %s represents the date and the second %s the time of the message (either when it was received or sent) var datetime_label = new Gtk.Label (new DateTime.from_unix_utc (relevant_timestamp).to_local ().format (_("%s at %s").printf (date_format, time_format))); - datetime_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); + datetime_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); var starred_icon = new Gtk.Image (); - starred_icon.icon_size = Gtk.IconSize.MENU; if (Camel.MessageFlags.FLAGGED in (int) message_info.flags) { starred_icon.icon_name = "starred-symbolic"; @@ -201,7 +198,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { var starred_button = new Gtk.Button () { child = starred_icon }; - starred_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + starred_button.add_css_class (Granite.STYLE_CLASS_FLAT); var upper_section = new Menu (); upper_section.append (_("Reply"), Action.print_detailed_name ( @@ -224,15 +221,14 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { actions_menu.append_section (null, lower_section); var actions_menu_button = new Gtk.MenuButton () { - image = new Gtk.Image.from_icon_name ("view-more-symbolic", Gtk.IconSize.MENU), + icon_name = "view-more-symbolic", tooltip_text = _("More"), margin_top = 6, valign = START, halign = END, - menu_model = actions_menu, - use_popover = false + menu_model = actions_menu }; - actions_menu_button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + actions_menu_button.add_css_class (Granite.STYLE_CLASS_FLAT); var action_grid = new Gtk.Grid () { column_spacing = 3, @@ -251,15 +247,13 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { margin_end = 12, column_spacing = 12 }; + header.set_cursor_from_name ("pointer"); header.attach (avatar, 0, 0, 1, 3); header.attach (header_stack, 1, 0, 1, 3); header.attach (action_grid, 2, 0); - var header_event_box = new Gtk.EventBox (); - header_event_box.events |= Gdk.EventMask.ENTER_NOTIFY_MASK; - header_event_box.events |= Gdk.EventMask.LEAVE_NOTIFY_MASK; - header_event_box.events |= Gdk.EventMask.BUTTON_RELEASE_MASK; - header_event_box.add (header); + var header_gesture_click = new Gtk.GestureClick (); + header.add_controller (header_gesture_click); var separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL) { hexpand = true @@ -267,23 +261,18 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { settings = new GLib.Settings ("io.elementary.mail"); - blocked_images_infobar = new Gtk.InfoBar () { + blocked_images_infobar = new Gtk.InfoBar () { //@TODO replacement: new styleclass? margin_top = 12, margin_bottom = 12, margin_start = 12, margin_end = 12, - message_type = WARNING + message_type = WARNING, + revealed = false }; - blocked_images_infobar.add_button (_("Show Images"), 1); + blocked_images_infobar.add_child (new Gtk.Label (_("This message contains remote images.")) { ellipsize = END }); //@TODO: Ellipsize: designwise not so sure here + blocked_images_infobar.add_button (_("Show Images"), 1); // Vertical content area doesn't work anymore blocked_images_infobar.add_button (_("Always Show from Sender"), 2); - blocked_images_infobar.get_style_context ().add_class (Gtk.STYLE_CLASS_FRAME); - blocked_images_infobar.no_show_all = true; - - var infobar_content = blocked_images_infobar.get_content_area (); - infobar_content.add (new Gtk.Label (_("This message contains remote images."))); - infobar_content.show_all (); - - ((Gtk.Box) blocked_images_infobar.get_action_area ()).orientation = Gtk.Orientation.VERTICAL; + blocked_images_infobar.add_css_class (Granite.STYLE_CLASS_FRAME); web_view = new Mail.WebView () { margin_top = 12, @@ -298,75 +287,59 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { }); var secondary_box = new Gtk.Box (VERTICAL, 0); - secondary_box.add (separator); - secondary_box.add (blocked_images_infobar); - secondary_box.add (web_view); + secondary_box.append (separator); + secondary_box.append (blocked_images_infobar); + secondary_box.append (web_view); secondary_revealer = new Gtk.Revealer () { - transition_type = SLIDE_UP + transition_type = SLIDE_UP, + child = secondary_box }; - secondary_revealer.add (secondary_box); var base_box = new Gtk.Box (VERTICAL, 0) { hexpand = true, vexpand = true }; - base_box.add (header_event_box); - base_box.add (secondary_revealer); + base_box.append (header); + base_box.append (secondary_revealer); if (Camel.MessageFlags.ATTACHMENTS in (int) message_info.flags) { - var attachment_icon = new Gtk.Image.from_icon_name ("mail-attachment-symbolic", Gtk.IconSize.MENU); + var attachment_icon = new Gtk.Image.from_icon_name ("mail-attachment-symbolic"); attachment_icon.margin_start = 6; attachment_icon.tooltip_text = _("This message contains one or more attachments"); action_grid.attach (attachment_icon, 1, 0); attachment_bar = new Gtk.FlowBox () { hexpand = true, - homogeneous = true + homogeneous = true, + activate_on_single_click = true }; - attachment_bar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); - attachment_bar.get_style_context ().add_class ("bottom-toolbar"); - secondary_box.add (attachment_bar); + attachment_bar.add_css_class (Granite.STYLE_CLASS_FLAT); + attachment_bar.add_css_class ("bottom-toolbar"); + secondary_box.append (attachment_bar); + + attachment_bar.child_activated.connect ((child) => { + show_attachment (((AttachmentButton)child).mime_part); + }); } - add (base_box); + set_child (base_box); expanded = false; - show_all (); - - avatar.set_loadable_icon (new GravatarIcon (parsed_address, get_style_context ().get_scale ())); - - /* Override default handler to stop event propagation. Otherwise clicking the menu will - expand or collapse the MessageListItem. */ - actions_menu_button.button_release_event.connect ((event) => { - actions_menu_button.set_active (true); - return Gdk.EVENT_STOP; - }); - - header_event_box.enter_notify_event.connect ((event) => { - if (event.detail != Gdk.NotifyType.INFERIOR) { - var window = header_event_box.get_window (); - var cursor = new Gdk.Cursor.from_name (window.get_display (), "pointer"); - window.set_cursor (cursor); - } - }); - header_event_box.leave_notify_event.connect ((event) => { - if (event.detail != Gdk.NotifyType.INFERIOR) { - header_event_box.get_window ().set_cursor (null); - } + get_gravatar.begin (parsed_address, (obj, res) => { + var gravatar = get_gravatar.end (res); + avatar.set_custom_image (gravatar); }); - header_event_box.button_release_event.connect ((event) => { + header_gesture_click.released.connect (() => { expanded = !expanded; - return Gdk.EVENT_STOP; }); destroy.connect (() => { loading_cancellable.cancel (); }); - /* Connecting to clicked () doesn't allow us to prevent the event from propagating to header_event_box */ - starred_button.button_release_event.connect (() => { + starred_button.clicked.connect (() => { if (Camel.MessageFlags.FLAGGED in (int) message_info.flags) { message_info.set_flags (Camel.MessageFlags.FLAGGED, 0); starred_icon.icon_name = "non-starred-symbolic"; @@ -376,12 +349,10 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { starred_icon.icon_name = "starred-symbolic"; starred_icon.tooltip_text = _("Unstar message"); } - - return Gdk.EVENT_STOP; }); web_view.image_load_blocked.connect (() => { - blocked_images_infobar.show (); + blocked_images_infobar.revealed = true; }); web_view.link_activated.connect ((uri) => { try { @@ -415,7 +386,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { /* @TODO: include header fields in printed output */ var print_operation = new WebKit.PrintOperation (web_view); print_operation.set_print_settings (settings); - print_operation.run_dialog ((Gtk.ApplicationWindow) get_toplevel ()); + print_operation.run_dialog ((Gtk.ApplicationWindow) get_root ()); } catch (Error e) { var print_error_dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("Unable to print email"), @@ -423,7 +394,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { "printer" ) { badge_icon = new ThemedIcon ("dialog-error"), - transient_for = (Gtk.Window) get_toplevel () + transient_for = (Gtk.Window) get_root () }; print_error_dialog.show_error_details (e.message); print_error_dialog.present (); @@ -440,7 +411,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { } } - private bool on_webview_context_menu (WebKit.ContextMenu menu, Gdk.Event event, WebKit.HitTestResult hit_test) { + private bool on_webview_context_menu (WebKit.ContextMenu menu, WebKit.HitTestResult hit_test) { WebKit.ContextMenu new_context_menu = new WebKit.ContextMenu (); for (int i = 0; i < menu.get_n_items (); i++) { @@ -501,7 +472,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { } web_view.load_images (); - blocked_images_infobar.destroy (); + blocked_images_infobar.revealed = false; }); } @@ -547,8 +518,7 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { yield handle_inline_mime (part); } else if (part.disposition == "attachment") { var button = new AttachmentButton (part, loading_cancellable); - button.activate.connect (() => show_attachment (button.mime_part)); - attachment_bar.add (button); + attachment_bar.append (button); } if (field.type == "text") { yield handle_text_mime (part.content); @@ -639,6 +609,25 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { return yield web_view.get_body_html (); } + private async Gtk.IconPaintable? get_gravatar (string address) { //@TODO: Worked once then never again; no idea :) + var uri = "https://www.gravatar.com/avatar/%s?d=404".printf ( + Checksum.compute_for_string (ChecksumType.MD5, address.strip ().down ()) + ); + var server_file = File.new_for_uri (uri); + var path = Path.build_filename (Environment.get_tmp_dir (), server_file.get_basename ()); + var local_file = File.new_for_path (path); + + if (!local_file.query_exists (loading_cancellable)) { + try { + yield server_file.copy_async (local_file, FileCopyFlags.OVERWRITE, GLib.Priority.DEFAULT, loading_cancellable, null); + } catch (Error e) { + warning (e.message); + return null; + } + } + return new Gtk.IconPaintable.for_file (local_file, avatar.size, get_style_context ().get_scale ()); + } + private void show_attachment (Camel.MimePart mime_part) { var dialog = new Granite.MessageDialog ( _("Trust and open “%s”?").printf (mime_part.get_filename ()), @@ -646,11 +635,11 @@ public class Mail.MessageListItem : Gtk.ListBoxRow { new ThemedIcon ("dialog-warning"), Gtk.ButtonsType.CANCEL ) { - transient_for = (Gtk.Window) get_toplevel () + transient_for = (Gtk.Window) get_root () }; var open_button = dialog.add_button (_("Open Anyway"), Gtk.ResponseType.OK); - open_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + open_button.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); dialog.present (); dialog.response.connect ((response_id) => { diff --git a/src/SourceList/CellRendererBadge.vala b/src/SourceList/CellRendererBadge.vala deleted file mode 100644 index 0dd449252..000000000 --- a/src/SourceList/CellRendererBadge.vala +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2019 elementary, Inc. (https://elementary.io) - * Copyright 2012–2013 Victor Eduardo - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -/** - * A badge renderer. - * - * Informs the user quickly on the content of the corresponding view. For example - * it might be used to show how much songs are in a playlist or how much updates - * are available. - * - * {{../doc/images/cellrendererbadge.png}} - * - * @since 0.2 - */ -public class Mail.CellRendererBadge : Gtk.CellRenderer { - public string text { get; set; default = ""; } - - private Pango.Rectangle text_logical_rect; - private Pango.Layout text_layout; - private Gtk.Border margin; - private Gtk.Border padding; - private Gtk.Border border; - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width ( - Gtk.Widget widget, - out int minimum_size, - out int natural_size - ) { - update_layout_properties (widget); - - int width = text_logical_rect.width; - width += margin.left + margin.right; - width += padding.left + padding.right; - width += border.left + border.right; - - minimum_size = natural_size = width + 2 * (int) xpad; - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, int width, - out int minimum_height, - out int natural_height - ) { - update_layout_properties (widget); - - int height = text_logical_rect.height; - height += margin.top + margin.bottom; - height += padding.top + padding.bottom; - height += border.top + border.bottom; - - minimum_height = natural_height = height + 2 * (int) ypad; - } - - private void update_layout_properties (Gtk.Widget widget) { - var ctx = widget.get_style_context (); - ctx.save (); - - // Add class before creating the pango layout and fetching paddings. - // This is needed in order to fetch the proper style information. - ctx.add_class (Granite.STYLE_CLASS_BADGE); - - var state = ctx.get_state (); - - margin = ctx.get_margin (state); - padding = ctx.get_padding (state); - border = ctx.get_border (state); - - text_layout = widget.create_pango_layout (text); - - ctx.restore (); - - Pango.Rectangle ink_rect; - text_layout.get_pixel_extents (out ink_rect, out text_logical_rect); - } - - public override void render ( - Cairo.Context context, - Gtk.Widget widget, - Gdk.Rectangle bg_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - update_layout_properties (widget); - - Gdk.Rectangle aligned_area = get_aligned_area (widget, flags, cell_area); - - int x = aligned_area.x; - int y = aligned_area.y; - int width = aligned_area.width; - int height = aligned_area.height; - - // Apply margin - x += margin.right; - y += margin.top; - width -= margin.left + margin.right; - height -= margin.top + margin.bottom; - - var ctx = widget.get_style_context (); - ctx.add_class (Granite.STYLE_CLASS_BADGE); - - ctx.render_background (context, x, y, width, height); - ctx.render_frame (context, x, y, width, height); - - // Apply border width and padding offsets - x += border.right + padding.right; - y += border.top + padding.top; - width -= border.left + border.right + padding.left + padding.right; - height -= border.top + border.bottom + padding.top + padding.bottom; - - // Center text - x += text_logical_rect.x + (width - text_logical_rect.width) / 2; - y += text_logical_rect.y + (height - text_logical_rect.height) / 2; - - ctx.render_layout (context, x, y, text_layout); - } -} diff --git a/src/SourceList/CellRendererExpander.vala b/src/SourceList/CellRendererExpander.vala deleted file mode 100644 index 3a2393012..000000000 --- a/src/SourceList/CellRendererExpander.vala +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2012–2019 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -/** - * An expander renderer. - * - * For it to draw an expander, the the {@link Gtk.CellRenderer.is_expander} property must - * be set to true; otherwise nothing is drawn. The state of the expander (i.e. expanded or - * collapsed) is controlled by the {@link Gtk.CellRenderer.is_expanded} property. - * - * @since 0.2 - */ -public class Mail.CellRendererExpander : Gtk.CellRenderer { - public bool is_category_expander { get; set; default = false; } - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width ( - Gtk.Widget widget, - out int minimum_size, - out int natural_size - ) { - apply_style_changes (widget); - minimum_size = natural_size = get_arrow_size (widget) + 2 * (int) xpad; - revert_style_changes (widget); - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, int width, - out int minimum_height, - out int natural_height - ) { - apply_style_changes (widget); - minimum_height = natural_height = get_arrow_size (widget) + 2 * (int) ypad; - revert_style_changes (widget); - } - - /** - * Gets the size of the expander arrow. - * - * The default implementation tries to retrieve the "expander-size" style property from - * //widget//, as it is primarily meant to be used along with a {@link Gtk.TreeView}. - * For those with special needs, it is recommended to override this method. - * - * @param widget Widget used to query the "expander-size" style property (should be a Gtk.TreeView.) - * @return Size of the expander arrow. - * @since 0.2 - */ - public virtual int get_arrow_size (Gtk.Widget widget) { - int arrow_size; - widget.style_get ("expander-size", out arrow_size); - return arrow_size; - } - - public override void render ( - Cairo.Context context, - Gtk.Widget widget, - Gdk.Rectangle bg_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - if (!is_expander) { - return; - } - - unowned Gtk.StyleContext ctx = apply_style_changes (widget); - - Gdk.Rectangle aligned_area = get_aligned_area (widget, flags, cell_area); - - int arrow_size = int.min (get_arrow_size (widget), aligned_area.width); - - int offset = arrow_size / 2; - int x = aligned_area.x + aligned_area.width / 2 - offset; - int y = aligned_area.y + aligned_area.height / 2 - offset; - - var state = ctx.get_state (); - const Gtk.StateFlags EXPANDED_FLAG = Gtk.StateFlags.CHECKED; - ctx.set_state (is_expanded ? state | EXPANDED_FLAG : state & ~EXPANDED_FLAG); - - ctx.render_expander (context, x, y, arrow_size, arrow_size); - - revert_style_changes (widget); - } - - private unowned Gtk.StyleContext apply_style_changes (Gtk.Widget widget) { - unowned Gtk.StyleContext ctx = widget.get_style_context (); - ctx.save (); - - if (is_category_expander) - ctx.add_class (Granite.STYLE_CLASS_CATEGORY_EXPANDER); - else - ctx.add_class (Gtk.STYLE_CLASS_EXPANDER); - - return ctx; - } - - private void revert_style_changes (Gtk.Widget widget) { - widget.get_style_context ().restore (); - } -} diff --git a/src/SourceList/SourceList.vala b/src/SourceList/SourceList.vala deleted file mode 100644 index 24a10c285..000000000 --- a/src/SourceList/SourceList.vala +++ /dev/null @@ -1,2599 +0,0 @@ -/* - * Copyright 2019 elementary, Inc. (https://elementary.io) - * Copyright 2012-2014 Victor Martinez - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -/** - * An interface for sorting items. - * - * @since 0.3 - */ -private interface Mail.SourceListSortable : Mail.SourceList.ExpandableItem { - /** - * Emitted after a user has re-ordered an item via DnD. - * - * @param moved The item that was moved to a different position by the user. - * @since 0.3 - */ - public signal void user_moved_item (SourceList.Item moved); - - /** - * Whether this item will allow users to re-arrange its children via DnD. - * - * This feature can co-exist with a sort algorithm (implemented - * by {@link Granite.Widgets.SourceListSortable.compare}), but - * the actual order of the items in the list will always - * honor that method. The sort function has to be compatible with - * the kind of DnD reordering the item wants to allow, since the user can - * only reorder those items for which //compare// returns 0. - * - * @return Whether the item's children can be re-arranged by users. - * @since 0.3 - */ - public abstract bool allow_dnd_sorting (); - - /** - * Should return a negative integer, zero, or a positive integer if ''a'' - * sorts //before// ''b'', ''a'' sorts //with// ''b'', or ''a'' sorts - * //after// ''b'' respectively. If two items compare as equal, their - * order in the sorted source list is undefined. - * - * In order to ensure that the source list behaves as expected, this - * method must define a partial order on the source list tree; i.e. it - * must be reflexive, antisymmetric and transitive. Not complying with - * those requirements could make the program fall into an infinite loop - * and freeze the user interface. - * - * Should return //0// to allow any pair of items to be sortable via DnD. - * - * @param a First item. - * @param b Second item. - * @return A //negative// integer if //a// sorts before //b//, - * //zero// if //a// equals //b//, or a //positive// - * integer if //a// sorts after //b//. - * @since 0.3 - */ - public abstract int compare (SourceList.Item a, SourceList.Item b); -} - -/** - * An interface for dragging items out of the source list widget. - * - * @since 0.3 - */ -public interface Mail.SourceListDragSource : Mail.SourceList.Item { - /** - * Determines whether this item can be dragged outside the source list widget. - * - * Even if this method returns //false//, the item could still be dragged around - * within the source list if its parent allows DnD reordering. This only happens - * when the parent implements {@link Granite.Widgets.SourceListSortable}. - * - * @return //true// if the item can be dragged; //false// otherwise. - * @since 0.3 - * @see Granite.Widgets.SourceListSortable - */ - public abstract bool draggable (); - - /** - * This method is called when the drop site requests the data which is dragged. - * - * It is the responsibility of this method to fill //selection_data// with the - * data in the format which is indicated by {@link Gtk.SelectionData.get_target}. - * - * @param selection_data {@link Gtk.SelectionData} containing source data. - * @since 0.3 - * @see Gtk.SelectionData.set - * @see Gtk.SelectionData.set_uris - * @see Gtk.SelectionData.set_text - */ - public abstract void prepare_selection_data (Gtk.SelectionData selection_data); -} - -/** - * An interface for receiving data from other widgets via drag-and-drop. - * - * @since 0.3 - */ -public interface Mail.SourceListDragDest : Mail.SourceList.Item { - /** - * Determines whether //data// can be dropped into this item. - * - * @param context The drag context. - * @param data {@link Gtk.SelectionData} containing source data. - * @return //true// if the drop is possible; //false// otherwise. - * @since 0.3 - */ - public abstract bool data_drop_possible (Gdk.DragContext context, Gtk.SelectionData data); - - /** - * If a data drop is deemed possible, then this method is called - * when the data is actually dropped into this item. Any actions - * consequence of the data received should be handled here. - * - * @param context The drag context. - * @param data {@link Gtk.SelectionData} containing source data. - * @return The action taken, or //0// to indicate that the dropped data was not accepted. - * @since 0.3 - */ - public abstract Gdk.DragAction data_received (Gdk.DragContext context, Gtk.SelectionData data); -} - -/** - * A widget that can display a list of items organized in categories. - * - * The source list widget consists of a collection of items, some of which are also expandable (and - * thus can contain more items). All the items displayed in the source list are children of the widget's - * root item. The API is meant to be used as follows: - * - * 1. Create the items you want to display in the source list, setting the appropriate values for their - * properties. The desired hierarchy is achieved by creating expandable items and adding items to them. - * These will be displayed as descendants in the widget's tree structure. The expandable items that are - * not nested inside any other item are considered to be at root level, and should be added to - * the widget's root item.<
> - * - * Expandable items located at the root level are treated as categories, and only support text. - * - * ''Example''<
> - * The final tree will have the following structure: - * {{{ - * Libraries - * Music - * Stores - * My Store - * Music - * Podcasts - * Devices - * Player 1 - * Player 2 - * }}} - * - * {{{ - * var library_category = new Granite.Widgets.SourceList.ExpandableItem ("Libraries"); - * var store_category = new Granite.Widgets.SourceList.ExpandableItem ("Stores"); - * var device_category = new Granite.Widgets.SourceList.ExpandableItem ("Devices"); - * - * var music_item = new Granite.Widgets.SourceList.Item ("Music"); - * - * // "Libraries" will be the parent category of "Music" - * library_category.add (music_item); - * - * // We plan to add sub-items to the store, so let's use an expandable item - * var my_store_item = new Granite.Widgets.SourceList.ExpandableItem ("My Store"); - * store_category.add (my_store_item); - * - * var my_store_podcast_item = new Granite.Widgets.SourceList.Item ("Podcasts"); - * var my_store_music_item = new Granite.Widgets.SourceList.Item ("Music"); - * - * my_store_item.add (my_store_music_item); - * my_store_item.add (my_store_podcast_item); - * - * var player1_item = new Granite.Widgets.SourceList.Item ("Player 1"); - * var player2_item = new Granite.Widgets.SourceList.Item ("Player 2"); - * - * device_category.add (player1_item); - * device_category.add (player2_item); - * }}} - * - * 2. Create a source list widget.<
> - * {{{ - * var source_list = new Granite.Widgets.SourceList (); - * }}} - * - * 3. Add root-level items to the {@link Granite.Widgets.SourceList.root} item. - * This item only serves as a container, and all its properties are ignored by the widget. - * - * {{{ - * // This will add the main categories (including their children) to the source list. After - * // having being added to be widget, any other item added to any of these items - * // (or any other child item in a deeper level) will be automatically added too. - * // There's no need to deal with the source list widget directly. - * - * var root = source_list.root; - * - * root.add (library_category); - * root.add (store_category); - * root.add (device_category); - * }}} - * - * The steps mentioned above are enough for initializing the source list. Future changes to the items' - * properties are ''automatically'' reflected by the widget. - * - * Final steps would involve connecting handlers to the source list events, being - * {@link Granite.Widgets.SourceList.item_selected} the most important, as it indicates that - * the selection was modified. - * - * Pack the source list into the GUI using the {@link Gtk.Paned} widget. - * This is usually done as follows: - * {{{ - * var pane = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); - * pane.pack1 (source_list, false, false); - * pane.pack2 (content_area, true, false); - * }}} - * - * @since 0.2 - * @see Gtk.Paned - */ -public class Mail.SourceList : Gtk.ScrolledWindow { - - /** - * = WORKING INTERNALS = - * - * In order to offer a transparent Item-based API, and avoid the need of providing methods - * to deal with items directly on the SourceList widget, it was decided to follow a monitor-like - * implementation, where the source list permanently monitors its root item and any other - * child item added to it. The task of monitoring the properties of the items has been - * divided among different objects, as shown below: - * - * Monitored by: Object::method that receives the signals indicating the property change. - * Applied by: Object::method that actually updates the tree to reflect the property changes - * (directly or indirectly, as in the case of the tree data model). - * - * --------------------------------------------------------------------------------------------- - * PROPERTY | MONITORED BY | APPLIED BY - * --------------------------------------------------------------------------------------------- - * + Item | | - * - parent | Not monitored | N/A - * - name | DataModel::on_item_prop_changed | Tree::name_cell_data_func - * - editable | DataModel::on_item_prop_changed | Queried when needed (See Tree::start_editing_item) - * - visible | DataModel::on_item_prop_changed | DataModel::filter_visible_func - * - icon | DataModel::on_item_prop_changed | Tree::icon_cell_data_func - * - activatable | Same as @icon | Same as @icon - * + ExpandableItem | | - * - collapsible | DataModel::on_item_prop_changed | Tree::update_expansion - * | | Tree::expander_cell_data_func - * - expanded | Same as @collapsible | Same as @collapsible - * --------------------------------------------------------------------------------------------- - * * Only automatic properties are monitored. ExpandableItem's additions/removals are handled by - * DataModel::add_item() and DataModel::remove_item() - * - * Other features: - * - Sorting: this happens on the tree-model level (DataModel). - */ - - - - /** - * A source list entry. - * - * Any change made to any of its properties will be ''automatically'' reflected - * by the {@link Granite.Widgets.SourceList} widget. - * - * @since 0.2 - */ - public class Item : Object { - - /** - * Emitted when the user has finished editing the item's name. - * - * By default, if the name doesn't consist of white space, it is automatically assigned - * to the {@link Granite.Widgets.SourceList.Item.name} property. The default behavior can - * be changed by overriding this signal. - * @param new_name The item's new name (result of editing.) - * @since 0.2 - */ - public virtual signal void edited (string new_name) { - if (editable && new_name.strip () != "") - this.name = new_name; - } - - /** - * The {@link Granite.Widgets.SourceList.Item.activatable} icon was activated. - * - * @see Granite.Widgets.SourceList.Item.activatable - * @since 0.2 - */ - public virtual signal void action_activated () { } - - /** - * Emitted when the item is double-clicked or when it is selected and one of the keys: - * Space, Shift+Space, Return or Enter is pressed. This signal is //also// for - * editable items. - * - * @since 0.2 - */ - public virtual signal void activated () { } - - /** - * Parent {@link Granite.Widgets.SourceList.ExpandableItem} of the item. - * ''Must not'' be modified. - * - * @since 0.2 - */ - public ExpandableItem parent { get; internal set; } - - /** - * The item's name. Primary and most important information. - * - * @since 0.2 - */ - public string name { get; set; default = ""; } - - /** - * The item's tooltip. If set to null (default), the tooltip for the item will be the - * contents of the {@link Granite.Widgets.SourceList.Item.name} property. - * - * @since 5.3 - */ - public string? tooltip { get; set; default = null; } - - /** - * Markup to be used instead of {@link Granite.Widgets.SourceList.ExpandableItem.name} - * This would mean that &, <, etc have to be escaped in the text, but basic formatting - * can be done on the item with HTML style tags. - * - * Note: Only the {@link Granite.Widgets.SourceList.ExpandableItem.name} property - * is modified for editable items. So this property will be need to updated and - * reformatted with editable items. - * - * @since 5.0 - */ - public string? markup { get; set; default = null; } - - /** - * A badge shown next to the item's name. - * - * It can be used for displaying the number of unread messages in the "Inbox" item, - * for instance. - * - * @since 0.2 - */ - public string badge { get; set; default = ""; } - - /** - * Whether the item's name can be edited from within the source list. - * - * When this property is set to //true//, users can edit the item by pressing - * the F2 key, or by double-clicking its name. - * - * ''This property only works for selectable items''. - * - * @see Granite.Widgets.SourceList.Item.selectable - * @see Granite.Widgets.SourceList.start_editing_item - * @since 0.2 - */ - public bool editable { get; set; default = false; } - - /** - * Whether the item should appear in the source list's tree or not. - * - * @since 0.2 - */ - public bool visible { get; set; default = true; } - - /** - * Whether the item can be selected or not. - * - * Setting this property to true doesn't guarantee that the item will actually be - * selectable, since there are other external factors to take into account, like the - * item's {@link Granite.Widgets.SourceList.Item.visible} property; whether the item is - * a category; the parent item is collapsed, etc. - * - * @see Granite.Widgets.SourceList.Item.visible - * @since 0.2 - */ - public bool selectable { get; set; default = true; } - - /** - * Primary icon. - * - * This property should be used to give the user an idea of what the item represents - * (i.e. content type.) - * - * @since 0.2 - */ - public Icon icon { get; set; } - - /** - * An activatable icon that works like a button. - * - * It can be used for e.g. showing an //"eject"// icon on a device's item. - * - * @see Granite.Widgets.SourceList.Item.action_activated - * @since 0.2 - */ - public Icon activatable { get; set; } - - /** - * The tooltip for the activatable icon. - * - * @since 5.0 - */ - public string activatable_tooltip { get; set; default = ""; } - - /** - * Creates a new {@link Granite.Widgets.SourceList.Item}. - * - * @param name Name of the item. - * @return (transfer full) A new {@link Granite.Widgets.SourceList.Item}. - * @since 0.2 - */ - private Item (string name = "") { - this.name = name; - } - - /** - * Invoked when the item is secondary-clicked or when the usual menu keys are pressed. - * - * Note that since Granite 5.0, right clicking on an item no longer selects/activates it, so - * any context menu items should be actioned on the item instance rather than the selected item - * in the SourceList - * - * @return A {@link Gtk.Menu} or //null// if nothing should be displayed. - * @since 0.2 - */ - public virtual Gtk.Menu? get_context_menu () { - return null; - } - } - - - - /** - * An item that can contain more items. - * - * It supports all the properties inherited from {@link Granite.Widgets.SourceList.Item}, - * and behaves like a normal item, except when it is located at the root level; in that case, - * the following properties are ignored by the widget: - * - * * {@link Granite.Widgets.SourceList.Item.selectable} - * * {@link Granite.Widgets.SourceList.Item.editable} - * * {@link Granite.Widgets.SourceList.Item.icon} - * * {@link Granite.Widgets.SourceList.Item.activatable} - * * {@link Granite.Widgets.SourceList.Item.badge} - * - * Root-level expandable items (i.e. Main Categories) are ''not'' displayed when they contain - * zero visible children. - * - * @since 0.2 - */ - public class ExpandableItem : Item { - - /** - * Emitted when an item is added. - * - * @param item Item added. - * @see Granite.Widgets.SourceList.ExpandableItem.add - * @since 0.2 - */ - public signal void child_added (Item item); - - /** - * Emitted when an item is removed. - * - * @param item Item removed. - * @see Granite.Widgets.SourceList.ExpandableItem.remove - * @since 0.2 - */ - public signal void child_removed (Item item); - - /** - * Emitted when the item is expanded or collapsed. - * - * @since 0.2 - */ - public virtual signal void toggled () { } - - /** - * Whether the item is collapsible or not. - * - * When set to //false//, the item is //always// expanded and the expander is - * not shown. Please note that this will also affect the value returned by the - * {@link Granite.Widgets.SourceList.ExpandableItem.expanded} property. - * - * @see Granite.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public bool collapsible { get; set; default = true; } - - /** - * Whether the item is expanded or not. - * - * The source list widget will obey the value of this property when possible. - * - * This property has no effect when {@link Granite.Widgets.SourceList.ExpandableItem.collapsible} - * is set to //false//. Also keep in mind that, __when set to //true//__, this property - * doesn't always represent the actual expansion state of an item. For example, it might - * be the case that an expandable item is collapsed because it has zero visible children, - * but its //expanded// property value is still //true//; in such case, once one of the - * item's children becomes visible, the item will be expanded again. Same applies to items - * hidden behind a collapsed parent item. - * - * If obtaining the ''actual'' expansion state of an item is important, - * use {@link Granite.Widgets.SourceList.is_item_expanded} instead. - * - * @see Granite.Widgets.SourceList.ExpandableItem.collapsible - * @see Granite.Widgets.SourceList.is_item_expanded - * @since 0.2 - */ - private bool _expanded = false; - public bool expanded { - get { return _expanded || !collapsible; } // if not collapsible, always return true - set { - if (value != _expanded) { - _expanded = value; - toggled (); - } - } - } - - /** - * Number of children contained by the item. - * - * @since 0.2 - */ - private uint n_children { - get { return children_list.size; } - } - - /** - * The item's children. - * - * This returns a newly-created list containing the children. - * It's safe to iterate it while removing items with - * {@link Granite.Widgets.SourceList.ExpandableItem.remove} - * - * @since 0.2 - */ - public Gee.Collection children { - owned get { - // Create a copy of the children so that it's safe to iterate it - // (e.g. by using foreach) while removing items. - var children_list_copy = new Gee.ArrayList (); - children_list_copy.add_all (children_list); - return children_list_copy; - } - } - - private Gee.Collection children_list = new Gee.ArrayList (); - - /** - * Creates a new {@link Granite.Widgets.SourceList.ExpandableItem} - * - * @param name Title of the item. - * @return (transfer full) A new {@link Granite.Widgets.SourceList.ExpandableItem}. - * @since 0.2 - */ - public ExpandableItem (string name = "") { - base (name); - } - - construct { - editable = false; - } - - /** - * Adds an item. - * - * {@link Granite.Widgets.SourceList.ExpandableItem.child_added} is fired after the item is added. - * - * While adding a child item, //the item it's being added to will set itself as the parent//. - * Please note that items are required to have their //parent// property set to //null// before - * being added, so make sure the item is removed from its previous parent before attempting - * to add it to another item. For instance: - * {{{ - * if (item.parent != null) - * item.parent.remove (item); // this will set item's parent to null - * new_parent.add (item); - * }}} - * - * @param item The item to add. Its parent __must__ be //null//. - * @see Granite.Widgets.SourceList.ExpandableItem.child_added - * @see Granite.Widgets.SourceList.ExpandableItem.remove - * @since 0.2 - */ - public void add (Item item) requires (item.parent == null) { - item.parent = this; - children_list.add (item); - child_added (item); - } - - /** - * Removes an item. - * - * The {@link Granite.Widgets.SourceList.ExpandableItem.child_removed} signal is fired - * //after removing the item//. Finally (i.e. after all the handlers have been invoked), - * the item's {@link Granite.Widgets.SourceList.Item.parent} property is set to //null//. - * This has the advantage of letting signal handlers know the parent from which //item// - * is being removed. - * - * @param item The item to remove. This will fail if item has a different parent. - * @see Granite.Widgets.SourceList.ExpandableItem.child_removed - * @see Granite.Widgets.SourceList.ExpandableItem.clear - * @since 0.2 - */ - public void remove (Item item) requires (item.parent == this) { - children_list.remove (item); - child_removed (item); - item.parent = null; - } - - /** - * Recursively expands the item along with its parent(s). - * - * @see Granite.Widgets.SourceList.ExpandableItem.expanded - * @since 0.2 - */ - public void expand_with_parents () { - // Update parent items first due to GtkTreeView's working internals: - // Expanding children before their parents would not always work, because - // they could be obscured behind a collapsed row by the time the treeview - // tries to expand them, obviously failing. - if (parent != null) - parent.expand_with_parents (); - expanded = true; - } - } - - - - /** - * The model backing the SourceList tree. - * - * It monitors item property changes, and handles children additions and removals. It also controls - * the visibility of the items based on their "visible" property, and on their number of children, - * if they happen to be categories. Its main purpose is to provide an easy and practical interface - * for sorting, adding, removing and updating items, eliminating the need of repeatedly dealing with - * the Gtk.TreeModel API directly. - */ - private class DataModel : Gtk.TreeModelFilter, Gtk.TreeDragSource, Gtk.TreeDragDest { - - /** - * An object that references a particular row in a model. This class is a wrapper built around - * Gtk.TreeRowReference, and exists with the purpose of ensuring we never use invalid tree paths - * or iters in the model, since most of these errors provoke failures due to GTK+ assertions - * or, even worse, unexpected behavior. - */ - private class NodeWrapper { - - /** - * The actual reference to the node. If is is null, it is treated as invalid. - */ - private Gtk.TreeRowReference? row_reference; - - /** - * A newly-created Gtk.TreeIter pointing to the node if it exists; null otherwise. - */ - public Gtk.TreeIter? iter { - owned get { - Gtk.TreeIter? rv = null; - - if (valid) { - var _path = this.path; - if (_path != null) { - Gtk.TreeIter _iter; - if (row_reference.get_model ().get_iter (out _iter, _path)) - rv = _iter; - } - } - - return rv; - } - } - - /** - * A newly-created Gtk.TreePath pointing to the node if it exists; null otherwise. - */ - public Gtk.TreePath? path { - owned get { return valid ? row_reference.get_path () : null; } - } - - /** - * Whether the node is valid or not. When it is not valid, no valid references are - * returned by the object to avoid errors (null is returned instead). - */ - private bool valid { - get { return row_reference != null && row_reference.valid (); } - } - - public NodeWrapper (Gtk.TreeModel model, Gtk.TreeIter iter) { - row_reference = new Gtk.TreeRowReference (model, model.get_path (iter)); - } - } - - /** - * Helper object used to monitor item property changes. - */ - private class ItemMonitor { - public signal void changed (Item self, string prop_name); - private Item item; - - public ItemMonitor (Item item) { - this.item = item; - item.notify.connect_after (on_notify); - } - - ~ItemMonitor () { - item.notify.disconnect (on_notify); - } - - private void on_notify (ParamSpec prop) { - changed (item, prop.name); - } - } - - private enum Column { - ITEM, - N_COLUMNS; - - public Type type () { - switch (this) { - case ITEM: - return typeof (Item); - - default: - assert_not_reached (); // a Type must be returned for every valid column - } - } - } - - public signal void item_updated (Item item); - - /** - * Used by push_parent_update() as key to associate the respective data to the objects. - */ - private const string ITEM_PARENT_NEEDS_UPDATE = "item-parent-needs-update"; - - private ExpandableItem _root; - - /** - * Root item. - * - * This item is not actually part of the model. It's only used as a proxy - * for adding and removing items. - */ - public ExpandableItem root { - get { return _root; } - set { - if (_root != null) { - remove_children_monitor (_root); - foreach (var item in _root.children) - remove_item (item); - } - - _root = value; - - add_children_monitor (_root); - foreach (var item in _root.children) - add_item (item); - } - } - - // This hash map stores items and their respective child node references. For that reason, the - // references it contains should only be used on the child_tree model, or converted to filter - // iters/paths using convert_child_*_to_*() before using them with the filter (i.e. this) model. - private Gee.HashMap items = new Gee.HashMap (); - - private Gee.HashMap monitors = new Gee.HashMap (); - - private Gtk.TreeStore child_tree; - private unowned SourceList.VisibleFunc? filter_func; - - construct { - child_tree = new Gtk.TreeStore (Column.N_COLUMNS, Column.ITEM.type ()); - child_model = child_tree; - virtual_root = null; - - child_tree.set_default_sort_func (child_model_sort_func); - resort (); - - set_visible_func (filter_visible_func); - } - - public bool has_item (Item item) { - return items.has_key (item); - } - - private void update_item (Item item) requires (has_item (item)) { - assert (root != null); - - // Emitting row_changed() for this item's row in the child model causes the filter - // (i.e. this model) to re-evaluate whether a row is visible or not, calling - // filter_visible_func() for that row again, and that's exactly what we want. - var node_reference = items.get (item); - if (node_reference != null) { - var path = node_reference.path; - var iter = node_reference.iter; - if (path != null && iter != null) { - child_tree.row_changed (path, iter); - item_updated (item); - } - } - } - - private void add_item (Item item) requires (!has_item (item)) { - assert (root != null); - - // Find the parent iter - Gtk.TreeIter? parent_child_iter = null, child_iter; - var parent = item.parent; - - if (parent != null && parent != root) { - // Add parent if it hasn't been added yet - if (!has_item (parent)) - add_item (parent); - - // Try to find the parent's iter - parent_child_iter = get_item_child_iter (parent); - - // Parent must have been added prior to adding this item - assert (parent_child_iter != null); - } - - child_tree.append (out child_iter, parent_child_iter); - child_tree.set (child_iter, Column.ITEM, item, -1); - - items.set (item, new NodeWrapper (child_tree, child_iter)); - - // This is equivalent to a property change. The tree still needs to update - // some of the new item's properties through this signal's handler. - item_updated (item); - - add_property_monitor (item); - - push_parent_update (parent); - - // If the item is expandable, also add children - var expandable = item as ExpandableItem; - if (expandable != null) { - foreach (var child_item in expandable.children) - add_item (child_item); - - // Monitor future additions/removals through signal handlers - add_children_monitor (expandable); - } - } - - private void remove_item (Item item) requires (has_item (item)) { - assert (root != null); - - remove_property_monitor (item); - - // get_item_child_iter() depends on items.get(item) for retrieving the right reference, - // so don't unset the item from @items yet! We first get the child iter and then - // unset the value. - var child_iter = get_item_child_iter (item); - - // Now we remove the item from the table, because that way get_item_child_iter() and - // all the methods that depend on it won't return invalid iters or items when - // called. This is important because child_tree.remove() will emit row_deleted(), - // and its handlers could potentially depend on one of the methods mentioned above. - items.unset (item); - - if (child_iter != null) - child_tree.remove (ref child_iter); - - push_parent_update (item.parent); - - // If the item is expandable, also remove children - var expandable = item as ExpandableItem; - if (expandable != null) { - // No longer monitor future additions or removals - remove_children_monitor (expandable); - - foreach (var child_item in expandable.children) - remove_item (child_item); - } - } - - private void add_property_monitor (Item item) { - var wrapper = new ItemMonitor (item); - monitors[item] = wrapper; - wrapper.changed.connect (on_item_prop_changed); - } - - private void remove_property_monitor (Item item) { - var wrapper = monitors[item]; - if (wrapper != null) - wrapper.changed.disconnect (on_item_prop_changed); - monitors.unset (item); - } - - private void add_children_monitor (ExpandableItem item) { - item.child_added.connect_after (on_item_child_added); - item.child_removed.connect_after (on_item_child_removed); - } - - private void remove_children_monitor (ExpandableItem item) { - item.child_added.disconnect (on_item_child_added); - item.child_removed.disconnect (on_item_child_removed); - } - - private void on_item_child_added (Item item) { - add_item (item); - } - - private void on_item_child_removed (Item item) { - remove_item (item); - } - - private void on_item_prop_changed (Item item, string prop_name) { - if (prop_name != "parent") - update_item (item); - } - - /** - * Pushes a call to update_item() if //parent// is not //null//. - * - * This is needed because the visibility of categories depends on their n_children property, - * and also because item expansion should be updated after adding or removing items. - * If many updates are pushed, and the item has still not been updated, only one is processed. - * This guarantees efficiency as updating a category item could trigger expensive actions. - */ - private void push_parent_update (ExpandableItem? parent) { - if (parent == null) - return; - - bool needs_update = parent.get_data (ITEM_PARENT_NEEDS_UPDATE); - - // If an update is already waiting to be processed, just return, as we - // don't need to queue another one for the same item. - if (needs_update) - return; - - var path = get_item_path (parent); - - if (path != null) { - // Let's mark this item for update - parent.set_data (ITEM_PARENT_NEEDS_UPDATE, true); - - Idle.add (() => { - if (parent != null) { - update_item (parent); - - // Already updated. No longer needs an update. - parent.set_data (ITEM_PARENT_NEEDS_UPDATE, false); - } - - return false; - }); - } - } - - /** - * Returns the Item pointed by iter, or null if the iter doesn't refer to a valid item. - */ - public Item? get_item (Gtk.TreeIter iter) { - Item? item; - get (iter, Column.ITEM, out item, -1); - return item; - } - - /** - * Returns the Item pointed by path, or null if the path doesn't refer to a valid item. - */ - public Item? get_item_from_path (Gtk.TreePath path) { - Gtk.TreeIter iter; - if (get_iter (out iter, path)) - return get_item (iter); - - return null; - } - - /** - * Returns a newly-created path pointing to the item, or null in case a valid path - * is not found. - */ - public Gtk.TreePath? get_item_path (Item item) { - Gtk.TreePath? path = null, child_path = get_item_child_path (item); - - // We want a filter path, not a child_model path - if (child_path != null) - path = convert_child_path_to_path (child_path); - - return path; - } - - /** - * External "extra" filter method. - */ - public void set_filter_func (SourceList.VisibleFunc? visible_func) { - this.filter_func = visible_func; - } - - /** - * Checks whether an item is a category (i.e. a root-level expandable item). - * The caller must pass an iter or path pointing to the item, but not both - * (one of them must be null.) - * - * TODO: instead of checking the position of the iter or path, we should simply - * check whether the item's parent is the root item and whether the item is - * expandable. We don't do so right now because vala still allows client code - * to access the Item.parent property, even though its setter is defined as internal. - */ - public bool is_category (Item item, Gtk.TreeIter? iter, Gtk.TreePath? path = null) { - bool is_category = false; - // either iter or path has to be null - if (item is ExpandableItem) { - if (iter != null) { - assert (path == null); - is_category = is_iter_at_root_level (iter); - } else { - assert (iter == null); - is_category = is_path_at_root_level (path); - } - } - return is_category; - } - - public bool is_iter_at_root_level (Gtk.TreeIter iter) { - return is_path_at_root_level (get_path (iter)); - } - - private bool is_path_at_root_level (Gtk.TreePath path) { - return path.get_depth () == 1; - } - - private void resort () { - child_tree.set_sort_column_id (Gtk.SortColumn.UNSORTED, Gtk.SortType.ASCENDING); - child_tree.set_sort_column_id (Gtk.SortColumn.DEFAULT, Gtk.SortType.ASCENDING); - } - - private int child_model_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) { - int order = 0; - - Item? item_a, item_b; - child_tree.get (a, Column.ITEM, out item_a, -1); - child_tree.get (b, Column.ITEM, out item_b, -1); - - // code should only compare items at same hierarchy level - assert (item_a.parent == item_b.parent); - - var parent = item_a.parent as SourceListSortable; - if (parent != null) - order = parent.compare (item_a, item_b); - - return order; - } - - private Gtk.TreeIter? get_item_child_iter (Item item) { - Gtk.TreeIter? child_iter = null; - - var child_node_wrapper = items.get (item); - if (child_node_wrapper != null) - child_iter = child_node_wrapper.iter; - - return child_iter; - } - - private Gtk.TreePath? get_item_child_path (Item item) { - Gtk.TreePath? child_path = null; - - var child_node_wrapper = items.get (item); - if (child_node_wrapper != null) - child_path = child_node_wrapper.path; - - return child_path; - } - - /** - * Filters the child-tree items based on their "visible" property. - */ - private bool filter_visible_func (Gtk.TreeModel child_model, Gtk.TreeIter iter) { - bool item_visible = false; - - Item? item; - child_tree.get (iter, Column.ITEM, out item, -1); - - if (item != null) { - item_visible = item.visible; - - // If the item is a category, also query the number of visible children - // because empty categories should not be displayed. - var expandable = item as ExpandableItem; - if (expandable != null && child_tree.iter_depth (iter) == 0) { - uint n_visible_children = 0; - foreach (var child_item in expandable.children) { - if (child_item.visible) - n_visible_children++; - } - item_visible = item_visible && n_visible_children > 0; - } - } - - if (filter_func != null) - item_visible = item_visible && filter_func (item); - - return item_visible; - } - - /** - * TreeDragDest implementation - */ - - private bool drag_data_received (Gtk.TreePath dest, Gtk.SelectionData selection_data) { - Gtk.TreeModel model; - Gtk.TreePath src_path; - - // Check if the user is dragging a row: - // - // Due to Gtk.TreeModelFilter's implementation of drag_data_get the values returned by - // tree_row_drag_data for GtkModel and GtkPath correspond to the child model and not the filter. - if (Gtk.tree_get_row_drag_data (selection_data, out model, out src_path) && model == child_tree) { - // get a child path representation of dest - var child_dest = convert_path_to_child_path (dest); - - if (child_dest != null) { - // New GtkTreeIters will be assigned to the rows at child_dest and its children. - if (child_tree_drag_data_received (child_dest, src_path)) - return true; - } - } - - // no new row inserted - return false; - } - - private bool child_tree_drag_data_received (Gtk.TreePath dest, Gtk.TreePath src_path) { - bool retval = false; - Gtk.TreeIter src_iter, dest_iter; - - if (!child_tree.get_iter (out src_iter, src_path)) - return false; - - var prev = dest; - - // Get the path to insert _after_ (dest is the path to insert _before_) - if (!prev.prev ()) { - // dest was the first spot at the current depth; which means - // we are supposed to prepend. - - var parent = dest; - Gtk.TreeIter? dest_parent = null; - - if (parent.up () && parent.get_depth () > 0) - child_tree.get_iter (out dest_parent, parent); - - child_tree.prepend (out dest_iter, dest_parent); - retval = true; - } else if (child_tree.get_iter (out dest_iter, prev)) { - var tmp_iter = dest_iter; - child_tree.insert_after (out dest_iter, null, tmp_iter); - retval = true; - } - - // If we succeeded in creating dest_iter, walk src_iter tree branch, - // duplicating it below dest_iter. - if (retval) { - recursive_node_copy (src_iter, dest_iter); - - // notify that the item was moved - Item item; - child_tree.get (src_iter, Column.ITEM, out item, -1); - return_val_if_fail (item != null, retval); - - // XXX Workaround: - // GtkTreeView automatically collapses expanded items that - // are dragged to a new location. Oddly, GtkTreeView doesn't fire - // 'row-collapsed' for the respective path, so we cannot keep track - // of that behavior via standard means. For now we'll just have - // our tree view check the properties of item again and ensure - // they're honored - update_item (item); - - var parent = item.parent as SourceListSortable; - return_val_if_fail (parent != null, retval); - - parent.user_moved_item (item); - } - - return retval; - } - - private void recursive_node_copy (Gtk.TreeIter src_iter, Gtk.TreeIter dest_iter) { - move_item (src_iter, dest_iter); - - Gtk.TreeIter child; - if (child_tree.iter_children (out child, src_iter)) { - // Need to create children and recurse. Note our dependence on - // persistent iterators here. - do { - Gtk.TreeIter copy; - child_tree.append (out copy, dest_iter); - recursive_node_copy (child, copy); - } while (child_tree.iter_next (ref child)); - } - } - - private void move_item (Gtk.TreeIter src_iter, Gtk.TreeIter dest_iter) { - Item item; - child_tree.get (src_iter, Column.ITEM, out item, -1); - return_if_fail (item != null); - - // update the row reference of item with the new location - child_tree.set (dest_iter, Column.ITEM, item, -1); - items.set (item, new NodeWrapper (child_tree, dest_iter)); - } - - private bool row_drop_possible (Gtk.TreePath dest, Gtk.SelectionData selection_data) { - Gtk.TreeModel model; - Gtk.TreePath src_path; - - // Check if the user is dragging a row: - // Due to Gtk.TreeModelFilter's implementation of drag_data_get the values returned by - // tree_row_drag_data for GtkModel and GtkPath correspond to the child model and not the filter. - if (!Gtk.tree_get_row_drag_data (selection_data, out model, out src_path) || model != child_tree) - return false; - - // get a representation of dest in the child model - var child_dest = convert_path_to_child_path (dest); - - // don't allow dropping an item into itself - if (child_dest == null || src_path.compare (child_dest) == 0) - return false; - - // Only allow DnD between items at the same depth (indentation level) - // This doesn't mean their parent is the same. - int src_depth = src_path.get_depth (); - int dest_depth = child_dest.get_depth (); - - if (src_depth != dest_depth) - return false; - - // no need to check dest_depth since we know its equal to src_depth - if (src_depth < 1) - return false; - - Item? parent = null; - - // if the depth is 1, we're talking about the items at root level, - // and by definition they share the same parent (root). We don't - // need to verify anything else for that specific case - if (src_depth == 1) { - parent = root; - } else { - // we verified equality above. this must be true - assert (dest_depth > 1); - - // Only allow reordering between siblings, i.e. items with the same - // parent. We don't want items to change their parent through DnD - // because that would complicate our existing APIs, and may introduce - // unpredictable behavior. - var src_indices = src_path.get_indices (); - var dest_indices = child_dest.get_indices (); - - // parent index is given by indices[depth-2], where depth > 1 - int src_parent_index = src_indices[src_depth - 2]; - int dest_parent_index = dest_indices[dest_depth - 2]; - - if (src_parent_index != dest_parent_index) - return false; - - // get parent. Note that we don't use the child path for this - var dest_parent = dest; - - if (!dest_parent.up () || dest_parent.get_depth () < 1) - return false; - - parent = get_item_from_path (dest_parent); - } - - var sortable = parent as SourceListSortable; - - if (sortable == null || !sortable.allow_dnd_sorting ()) - return false; - - var dest_item = get_item_from_path (dest); - - if (dest_item == null) - return true; - - Item? source_item = null; - var filter_src_path = convert_child_path_to_path (src_path); - - if (filter_src_path != null) - source_item = get_item_from_path (filter_src_path); - - if (source_item == null) - return false; - - // If order isn't indifferent (=0), 'dest' has to sort before 'source'. - // Otherwise we'd allow the user to move the 'source_item' to a new - // location before 'dest_item', but that location would be changed - // later by the sort function, making the whole interaction poinless. - // We better prevent such reorderings from the start by giving the - // user a visual clue about the invalid drop location. - if (sortable.compare (dest_item, source_item) >= 0) { - if (!dest.prev ()) - return true; - - // 'source_item' also has to sort 'after' or 'equal' the item currently - // preceding 'dest_item' - var dest_item_prev = get_item_from_path (dest); - - return dest_item_prev != null - && dest_item_prev != source_item - && sortable.compare (dest_item_prev, source_item) <= 0; - } - - return false; - } - - /** - * Override default implementation of TreeDragSource - * - * drag_data_delete is not overriden because the default implementation - * does exactly what we need. - */ - - private bool drag_data_get (Gtk.TreePath path, Gtk.SelectionData selection_data) { - // If we're asked for a data about a row, just have the default implementation fill in - // selection_data. Please note that it will provide information relative to child_model. - if (selection_data.get_target () == Gdk.Atom.intern_static_string ("GTK_TREE_MODEL_ROW")) - return base.drag_data_get (path, selection_data); - - // check if the item at path provides DnD source data - var drag_source_item = get_item_from_path (path) as SourceListDragSource; - if (drag_source_item != null && drag_source_item.draggable ()) { - drag_source_item.prepare_selection_data (selection_data); - return true; - } - - return false; - } - - private bool row_draggable (Gtk.TreePath path) { - if (!base.row_draggable (path)) - return false; - - var item = get_item_from_path (path); - - if (item != null) { - // check if the item's parent allows DnD sorting - var sortable_item = item.parent as SourceListSortable; - - if (sortable_item != null && sortable_item.allow_dnd_sorting ()) - return true; - - // Since the parent item does not allow DnD sorting, there's no - // reason to allow dragging it unless the row is actually draggable. - var drag_source_item = item as SourceListDragSource; - - if (drag_source_item != null && drag_source_item.draggable ()) - return true; - } - - return false; - } - } - - - /** - * Class responsible for rendering Item.icon and Item.activatable. It also - * notifies about clicks through the activated() signal. - */ - private class CellRendererIcon : Gtk.CellRendererPixbuf { - public signal void activated (string path); - - construct { - mode = Gtk.CellRendererMode.ACTIVATABLE; - stock_size = Gtk.IconSize.MENU; - } - - public override bool activate ( - Gdk.Event event, - Gtk.Widget widget, - string path, - Gdk.Rectangle background_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - activated (path); - return true; - } - } - - /** - * A cell renderer that only adds space. - */ - private class CellRendererSpacer : Gtk.CellRenderer { - /** - * Indentation level represented by this cell renderer - */ - public int level { get; set; default = -1; } - - public override Gtk.SizeRequestMode get_request_mode () { - return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void get_preferred_width (Gtk.Widget widget, out int min_size, out int natural_size) { - min_size = natural_size = 2 * (int) xpad; - } - - public override void get_preferred_height_for_width ( - Gtk.Widget widget, - int width, - out int min_height, - out int natural_height - ) { - min_height = natural_height = 2 * (int) ypad; - } - - public override void render ( - Cairo.Context context, - Gtk.Widget widget, - Gdk.Rectangle bg_area, - Gdk.Rectangle cell_area, - Gtk.CellRendererState flags - ) { - // Nothing to do. This renderer only adds space. - } - } - - - - /** - * The tree that actually displays the items. - * - * All the user interaction happens here. - */ - private class Tree : Gtk.TreeView { - public DataModel data_model { get; construct set; } - - public signal void item_selected (Item? item); - - public Item? selected_item { - get { return selected; } - set { set_selected (value, true); } - } - - public bool editing { - get { return text_cell.editing; } - } - - public Pango.EllipsizeMode ellipsize_mode { - get { return text_cell.ellipsize; } - set { text_cell.ellipsize = value; } - } - - private enum Column { - ITEM, - N_COLS - } - - private Item? selected; - private unowned Item? edited; - - private Gtk.Entry? editable_entry; - private Gtk.CellRendererText text_cell; - private CellRendererIcon icon_cell; - private CellRendererIcon activatable_cell; - private CellRendererBadge badge_cell; - private CellRendererExpander primary_expander_cell; - private CellRendererExpander secondary_expander_cell; - private Gee.HashMap spacer_cells; // cells used for left spacing - private bool unselectable_item_clicked = false; - - private const string DEFAULT_STYLESHEET = """ - .sidebar.badge { - border-radius: 10px; - border-width: 0; - padding: 1px 2px 1px 2px; - font-weight: bold; - } - """; - - private const string STYLE_PROP_LEVEL_INDENTATION = "level-indentation"; - private const string STYLE_PROP_LEFT_PADDING = "left-padding"; - private const string STYLE_PROP_EXPANDER_SPACING = "expander-spacing"; - - static construct { - install_style_property (new ParamSpecInt ( - STYLE_PROP_LEVEL_INDENTATION, - "Level Indentation", - "Space to add at the beginning of every indentation level. Must be an even number.", - 1, - 50, - 6, - ParamFlags.READABLE - )); - - install_style_property (new ParamSpecInt ( - STYLE_PROP_LEFT_PADDING, - "Left Padding", - "Padding added to the left side of the tree. Must be an even number.", - 1, - 50, - 4, - ParamFlags.READABLE - )); - - install_style_property (new ParamSpecInt ( - STYLE_PROP_EXPANDER_SPACING, - "Expander Spacing", - "Space added between an item and its expander. Must be an even number.", - 1, - 50, - 4, - ParamFlags.READABLE - )); - } - - public Tree (DataModel data_model) { - Object (data_model: data_model); - } - - construct { - unowned Gtk.StyleContext style_context = get_style_context (); - style_context.add_class (Gtk.STYLE_CLASS_SIDEBAR); - style_context.add_class (Granite.STYLE_CLASS_SOURCE_LIST); - - var css_provider = new Gtk.CssProvider (); - try { - css_provider.load_from_data (DEFAULT_STYLESHEET, -1); - style_context.add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK); - } catch (Error e) { - warning ("Could not create CSS Provider: %s\nStylesheet:\n%s", e.message, DEFAULT_STYLESHEET); - } - - set_model (data_model); - - halign = valign = Gtk.Align.FILL; - expand = true; - - enable_search = false; - headers_visible = false; - enable_grid_lines = Gtk.TreeViewGridLines.NONE; - - // Deactivate GtkTreeView's built-in expander functionality - expander_column = null; - show_expanders = false; - - var item_column = new Gtk.TreeViewColumn (); - item_column.expand = true; - - insert_column (item_column, Column.ITEM); - - // Now pack the cell renderers. We insert them in reverse order (using pack_end) - // because we want to use TreeViewColumn.pack_start exclusively for inserting - // spacer cell renderers for level-indentation purposes. - // See add_spacer_cell_for_level() for more details. - - // Second expander. Used for main categories - secondary_expander_cell = new CellRendererExpander (); - secondary_expander_cell.is_category_expander = true; - secondary_expander_cell.xpad = 10; - item_column.pack_end (secondary_expander_cell, false); - item_column.set_cell_data_func (secondary_expander_cell, expander_cell_data_func); - - activatable_cell = new CellRendererIcon (); - activatable_cell.xpad = 6; - activatable_cell.activated.connect (on_activatable_activated); - item_column.pack_end (activatable_cell, false); - item_column.set_cell_data_func (activatable_cell, icon_cell_data_func); - - badge_cell = new CellRendererBadge (); - badge_cell.xpad = 1; - badge_cell.xalign = 1; - item_column.pack_end (badge_cell, false); - item_column.set_cell_data_func (badge_cell, badge_cell_data_func); - - text_cell = new Gtk.CellRendererText (); - text_cell.editable_set = true; - text_cell.editable = false; - text_cell.editing_started.connect (on_editing_started); - text_cell.editing_canceled.connect (on_editing_canceled); - text_cell.ellipsize = Pango.EllipsizeMode.END; - text_cell.xalign = 0; - item_column.pack_end (text_cell, true); - item_column.set_cell_data_func (text_cell, name_cell_data_func); - - icon_cell = new CellRendererIcon (); - icon_cell.xpad = 2; - item_column.pack_end (icon_cell, false); - item_column.set_cell_data_func (icon_cell, icon_cell_data_func); - - // First expander. Used for normal expandable items - primary_expander_cell = new CellRendererExpander (); - - int expander_spacing; - style_get (STYLE_PROP_EXPANDER_SPACING, out expander_spacing); - primary_expander_cell.xpad = expander_spacing / 2; - - item_column.pack_end (primary_expander_cell, false); - item_column.set_cell_data_func (primary_expander_cell, expander_cell_data_func); - - // Selection - var selection = get_selection (); - selection.mode = Gtk.SelectionMode.BROWSE; - selection.set_select_function (select_func); - - // Monitor item changes - enable_item_property_monitor (); - - // Add root-level indentation. New levels will be added by update_item_expansion() - add_spacer_cell_for_level (1); - - // Enable basic row drag and drop - configure_drag_source (null); - configure_drag_dest (null, 0); - - query_tooltip.connect_after (on_query_tooltip); - has_tooltip = true; - } - - ~Tree () { - disable_item_property_monitor (); - } - - public override bool drag_motion (Gdk.DragContext context, int x, int y, uint time) { - // call the base signal to get rows with children to spring open - if (!base.drag_motion (context, x, y, time)) - return false; - - Gtk.TreePath suggested_path, current_path; - Gtk.TreeViewDropPosition suggested_pos, current_pos; - - if (get_dest_row_at_pos (x, y, out suggested_path, out suggested_pos)) { - // the base implementation of drag_motion was likely to set a drop - // destination row. If that's the case, we configure the row position - // to only allow drops before or after it, but not into it - get_drag_dest_row (out current_path, out current_pos); - - if (current_path != null && suggested_path.compare (current_path) == 0) { - // If the source widget is this treeview, we assume we're - // just dragging rows around, because at the moment dragging - // rows into other rows (re-parenting) is not implemented. - var source_widget = Gtk.drag_get_source_widget (context); - bool dragging_treemodel_row = (source_widget == this); - - if (dragging_treemodel_row) { - // we don't allow DnD into other rows, only in between them - // (no row is highlighted) - if (current_pos != Gtk.TreeViewDropPosition.BEFORE) { - if (current_pos == Gtk.TreeViewDropPosition.INTO_OR_BEFORE) - set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.BEFORE); - else - set_drag_dest_row (null, Gtk.TreeViewDropPosition.AFTER); - } - } else { - // for DnD originated on a different widget, we don't want to insert - // between rows, only select the rows themselves - if (current_pos == Gtk.TreeViewDropPosition.BEFORE) - set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.INTO_OR_BEFORE); - else if (current_pos == Gtk.TreeViewDropPosition.AFTER) - set_drag_dest_row (current_path, Gtk.TreeViewDropPosition.INTO_OR_AFTER); - - // determine if external DnD is supported by the item at destination - var dest = data_model.get_item_from_path (current_path) as SourceListDragDest; - - if (dest != null) { - var target_list = Gtk.drag_dest_get_target_list (this); - var target = Gtk.drag_dest_find_target (this, context, target_list); - - // have 'drag_get_data' call 'drag_data_received' to determine - // if the data can actually be dropped. - context.set_data ("suggested-dnd-action", context.get_suggested_action ()); - Gtk.drag_get_data (this, context, target, time); - } else { - // dropping data here is not supported. Unset dest row - set_drag_dest_row (null, Gtk.TreeViewDropPosition.BEFORE); - } - } - } - } else { - // dropping into blank areas of SourceList is not allowed - set_drag_dest_row (null, Gtk.TreeViewDropPosition.AFTER); - return false; - } - - return true; - } - - public override void drag_data_received ( - Gdk.DragContext context, - int x, - int y, - Gtk.SelectionData selection_data, - uint info, - uint time - ) { - var target_list = Gtk.drag_dest_get_target_list (this); - var target = Gtk.drag_dest_find_target (this, context, target_list); - - if (target == Gdk.Atom.intern_static_string ("GTK_TREE_MODEL_ROW")) { - base.drag_data_received (context, x, y, selection_data, info, time); - return; - } - - Gtk.TreePath path; - Gtk.TreeViewDropPosition pos; - - if (context.get_data ("suggested-dnd-action") != 0) { - context.set_data ("suggested-dnd-action", 0); - - get_drag_dest_row (out path, out pos); - - if (path != null) { - // determine if external DnD is allowed by the item at destination - var dest = data_model.get_item_from_path (path) as SourceListDragDest; - - if (dest == null || !dest.data_drop_possible (context, selection_data)) { - // dropping data here is not allowed. unset any previously - // selected destination row - set_drag_dest_row (null, Gtk.TreeViewDropPosition.BEFORE); - Gdk.drag_status (context, 0, time); - return; - } - } - - Gdk.drag_status (context, context.get_suggested_action (), time); - } else { - if (get_dest_row_at_pos (x, y, out path, out pos)) { - // Data coming from external source/widget was dropped into this item. - // selection_data contains something other than a tree row; most likely - // we're dealing with a DnD not originated within the Source List tree. - // Let's pass the data to the corresponding item, if there's a handler. - - var drag_dest = data_model.get_item_from_path (path) as SourceListDragDest; - - if (drag_dest != null) { - var action = drag_dest.data_received (context, selection_data); - Gtk.drag_finish (context, action != 0, action == Gdk.DragAction.MOVE, time); - return; - } - } - - // failure - Gtk.drag_finish (context, false, false, time); - } - } - - public void configure_drag_source (Gtk.TargetEntry[]? src_entries) { - // Append GTK_TREE_MODEL_ROW to src_entries and src_entries to enable row DnD. - var entries = append_row_target_entry (src_entries); - - unset_rows_drag_source (); - enable_model_drag_source (Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.MOVE); - } - - public void configure_drag_dest (Gtk.TargetEntry[]? dest_entries, Gdk.DragAction actions) { - // Append GTK_TREE_MODEL_ROW to dest_entries and dest_entries to enable row DnD. - var entries = append_row_target_entry (dest_entries); - - unset_rows_drag_dest (); - - // DragAction.MOVE needs to be enabled for row drag-and-drop to work properly - enable_model_drag_dest (entries, Gdk.DragAction.MOVE | actions); - } - - private bool on_query_tooltip (int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { - Gtk.TreePath path; - Gtk.TreeViewColumn column = get_column (Column.ITEM); - - get_tooltip_context (ref x, ref y, keyboard_tooltip, null, out path, null); - if (path == null) { - return false; - } - - var item = data_model.get_item_from_path (path); - if (item != null) { - bool should_show = false; - - Gdk.Rectangle start_cell_area; - get_cell_area (path, column, out start_cell_area); - - set_tooltip_row (tooltip, path); - - if (item.tooltip == null) { - tooltip.set_markup (item.name); - should_show = true; - } else if (item.tooltip != "") { - tooltip.set_markup (item.tooltip); - should_show = true; - } - - if (keyboard_tooltip) { - return should_show; - } - - if (over_cell (column, path, text_cell, x - start_cell_area.x) || - over_cell (column, path, icon_cell, x - start_cell_area.x)) { - - return should_show; - } else if (over_cell (column, path, activatable_cell, x - start_cell_area.x)) { - if (item.activatable_tooltip == "") { - return false; - } else { - tooltip.set_markup (item.activatable_tooltip); - return true; - } - } - } - - return false; - } - - private static Gtk.TargetEntry[] append_row_target_entry (Gtk.TargetEntry[]? orig) { - const Gtk.TargetEntry row_target_entry = { // vala-lint=naming-convention - "GTK_TREE_MODEL_ROW", - Gtk.TargetFlags.SAME_WIDGET, - 0 - }; - - var entries = new Gtk.TargetEntry[0]; - entries += row_target_entry; - - if (orig != null) { - foreach (var target_entry in orig) - entries += target_entry; - } - - return entries; - } - - private void enable_item_property_monitor () { - data_model.item_updated.connect_after (on_model_item_updated); - } - - private void disable_item_property_monitor () { - data_model.item_updated.disconnect (on_model_item_updated); - } - - private void on_model_item_updated (Item item) { - // Currently, all the other properties are updated automatically by the - // cell-data functions after a change in the model. - var expandable_item = item as ExpandableItem; - if (expandable_item != null) - update_expansion (expandable_item); - } - - private void add_spacer_cell_for_level ( - int level, - bool check_previous = true - ) requires (level > 0) { - if (spacer_cells == null) - spacer_cells = new Gee.HashMap (); - - if (!spacer_cells.has_key (level)) { - var spacer_cell = new CellRendererSpacer (); - spacer_cell.level = level; - spacer_cells[level] = spacer_cell; - - uint cell_xpadding; - - // The primary expander is not visible for root-level (i.e. first level) - // items, so for the second level of indentation we use a low padding - // because the primary expander will add enough space. For the root level, - // we use left_padding, and level_indentation for the remaining levels. - // The value of cell_xpadding will be allocated *twice* by the cell renderer, - // so we set the value to a half of actual (desired) value. - switch (level) { - case 1: // root - int left_padding; - style_get (STYLE_PROP_LEFT_PADDING, out left_padding); - cell_xpadding = left_padding / 2; - break; - - case 2: // second level - cell_xpadding = 0; - break; - - default: // remaining levels - int level_indentation; - style_get (STYLE_PROP_LEVEL_INDENTATION, out level_indentation); - cell_xpadding = level_indentation / 2; - break; - } - - spacer_cell.xpad = cell_xpadding; - - var item_column = get_column (Column.ITEM); - item_column.pack_start (spacer_cell, false); - item_column.set_cell_data_func (spacer_cell, spacer_cell_data_func); - - // Make sure that the previous indentation levels also exist - if (check_previous) { - for (int i = level - 1; i > 0; i--) - add_spacer_cell_for_level (i, false); - } - } - } - - /** - * Evaluates whether the item at the specified path can be selected or not. - */ - private bool select_func ( - Gtk.TreeSelection selection, - Gtk.TreeModel model, - Gtk.TreePath path, - bool path_currently_selected - ) { - bool selectable = false; - var item = data_model.get_item_from_path (path); - - if (item != null) { - // Main categories ARE NOT selectable, so check for that - if (!data_model.is_category (item, null, path)) - selectable = item.selectable; - } - - return selectable; - } - - private Gtk.TreePath? get_selected_path () { - Gtk.TreePath? selected_path = null; - Gtk.TreeSelection? selection = get_selection (); - - if (selection != null) { - Gtk.TreeModel? model; - var selected_rows = selection.get_selected_rows (out model); - if (selected_rows.length () == 1) - selected_path = selected_rows.nth_data (0); - } - - return selected_path; - } - - private void set_selected (Item? item, bool scroll_to_item) { - if (item == null) { - Gtk.TreeSelection? selection = get_selection (); - if (selection != null) - selection.unselect_all (); - - // As explained in cursor_changed(), we cannot emit signals for this special - // case from there because that wouldn't allow us to implement the behavior - // we want (i.e. restoring the old selection after expanding a previously - // collapsed category) without emitting the undesired item_selected() signal - // along the way. This special case is handled manually, because it *should* - // only happen in response to client code requests and never in response to - // user interaction. We do that here because there's no way to determine - // whether the cursor change came from code (i.e. this method) or user - // interaction from cursor_changed(). - this.selected = null; - item_selected (null); - } else if (item.selectable) { - if (scroll_to_item) - this.scroll_to_item (item); - - var to_select = data_model.get_item_path (item); - if (to_select != null) - set_cursor_on_cell (to_select, get_column (Column.ITEM), text_cell, false); - } - } - - public override void cursor_changed () { - var path = get_selected_path (); - Item? new_item = path != null ? data_model.get_item_from_path (path) : null; - - // Don't do anything if @new_item is null. - // - // The only way 'this.selected' can be null is by setting it explicitly to - // that value from client code, and thus we handle that case in set_selected(). - // THIS CANNOT HAPPEN IN RESPONSE TO USER INTERACTION. For example, if an - // item is un-selected because its parent category has been collapsed, then it will - // remain as the current selected item (not in reality, just as the value of - // this.selected) and will be re-selected after the parent is expanded again. - // THIS ALL HAPPENS SILENTLY BEHIND THE SCENES, so client code will never know - // it ever happened; the value of selected_item remains unchanged and item_selected() - // is not emitted. - if (new_item != null && new_item != this.selected) { - this.selected = new_item; - item_selected (new_item); - } - } - - public bool scroll_to_item (Item item, bool use_align = false, float row_align = 0) { - bool scrolled = false; - - var path = data_model.get_item_path (item); - if (path != null) { - scroll_to_cell (path, null, use_align, row_align, 0); - scrolled = true; - } - - return scrolled; - } - - public bool start_editing_item (Item item) requires (item.editable) requires (item.selectable) { - if (editing && item == edited) // If same item again, simply return. - return false; - - var path = data_model.get_item_path (item); - if (path != null) { - edited = item; - text_cell.editable = true; - set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, true); - } else { - warning ("Could not edit \"%s\": path not found", item.name); - } - - return editing; - } - - public void stop_editing () { - if (editing && edited != null) { - var path = data_model.get_item_path (edited); - - // Setting the cursor on the same cell without starting an edit cancels any - // editing operation going on. - if (path != null) - set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, false); - } - } - - private void on_editing_started (Gtk.CellEditable editable, string path) { - editable_entry = editable as Gtk.Entry; - if (editable_entry != null) { - editable_entry.editing_done.connect (on_editing_done); - editable_entry.editable = true; - } - } - - private void on_editing_canceled () { - if (editable_entry != null) { - editable_entry.editable = false; - editable_entry.editing_done.disconnect (on_editing_done); - } - - text_cell.editable = false; - edited = null; - } - - private void on_editing_done () { - if (edited != null && edited.editable && editable_entry != null) - edited.edited (editable_entry.get_text ()); - - // Same actions as when canceling editing - on_editing_canceled (); - } - - private void on_activatable_activated (string item_path_str) { - var item = get_item_from_path_string (item_path_str); - if (item != null) - item.action_activated (); - } - - private Item? get_item_from_path_string (string item_path_str) { - var item_path = new Gtk.TreePath.from_string (item_path_str); - return data_model.get_item_from_path (item_path); - } - - private bool toggle_expansion (ExpandableItem item) { - if (item.collapsible) { - item.expanded = !item.expanded; - return true; - } - return false; - } - - /** - * Updates the tree to reflect the ''expanded'' property of expandable_item. - */ - private void update_expansion (ExpandableItem expandable_item) { - var path = data_model.get_item_path (expandable_item); - - if (path != null) { - // Make sure that the indentation cell for the item's level exists. - // We use +1 because the method will make sure that the previous - // indentation levels exist too. - add_spacer_cell_for_level (path.get_depth () + 1); - - if (expandable_item.expanded) { - expand_row (path, false); - - // Since collapsing an item un-selects any child item previously selected, - // we need to restore the selection. This will be done silently because - // set_selected checks for equality between the previously "selected" - // item and the newly selected, and only emits the item_selected() signal - // if they are different. See cursor_changed() for a better explanation - // of this behavior. - if (selected != null && selected.parent == expandable_item) - set_selected (selected, true); - - // Collapsing expandable_item's row also collapsed all its children, - // and thus we need to update the "expanded" property of each of them - // to reflect their previous state. - foreach (var child_item in expandable_item.children) { - var child_expandable_item = child_item as ExpandableItem; - if (child_expandable_item != null) - update_expansion (child_expandable_item); - } - } else { - collapse_row (path); - } - } - } - - public override void row_expanded (Gtk.TreeIter iter, Gtk.TreePath path) { - var item = data_model.get_item (iter) as ExpandableItem; - return_if_fail (item != null); - - disable_item_property_monitor (); - item.expanded = true; - enable_item_property_monitor (); - } - - public override void row_collapsed (Gtk.TreeIter iter, Gtk.TreePath path) { - var item = data_model.get_item (iter) as ExpandableItem; - return_if_fail (item != null); - - disable_item_property_monitor (); - item.expanded = false; - enable_item_property_monitor (); - } - - public override void row_activated (Gtk.TreePath path, Gtk.TreeViewColumn column) { - if (column == get_column (Column.ITEM)) { - var item = data_model.get_item_from_path (path); - if (item != null) - item.activated (); - } - } - - public override bool key_release_event (Gdk.EventKey event) { - if (selected_item != null) { - switch (event.keyval) { - case Gdk.Key.F2: - var modifiers = Gtk.accelerator_get_default_mod_mask (); - // try to start editing selected item - if ((event.state & modifiers) == 0 && selected_item.editable) - start_editing_item (selected_item); - break; - } - } - - return base.key_release_event (event); - } - - public override bool button_release_event (Gdk.EventButton event) { - if (unselectable_item_clicked && event.window == get_bin_window ()) { - unselectable_item_clicked = false; - - Gtk.TreePath path; - Gtk.TreeViewColumn column; - int x = (int) event.x, y = (int) event.y, cell_x, cell_y; - - if (get_path_at_pos (x, y, out path, out column, out cell_x, out cell_y)) { - var item = data_model.get_item_from_path (path) as ExpandableItem; - - if (item != null) { - if (!item.selectable || data_model.is_category (item, null, path)) - toggle_expansion (item); - } - } - } - - return base.button_release_event (event); - } - - public override bool button_press_event (Gdk.EventButton event) { - if (event.window != get_bin_window ()) - return base.button_press_event (event); - - Gtk.TreePath path; - Gtk.TreeViewColumn column; - int x = (int) event.x, y = (int) event.y, cell_x, cell_y; - - if (get_path_at_pos (x, y, out path, out column, out cell_x, out cell_y)) { - var item = data_model.get_item_from_path (path); - - // This is needed because the treeview adds an offset at the beginning of every level - Gdk.Rectangle start_cell_area; - get_cell_area (path, get_column (0), out start_cell_area); - cell_x -= start_cell_area.x; - - if (item != null && column == get_column (Column.ITEM)) { - // Cancel any editing operation going on - stop_editing (); - - if (event.button == Gdk.BUTTON_SECONDARY) { - popup_context_menu (item, event); - return true; - } else if (event.button == Gdk.BUTTON_PRIMARY) { - // Check whether an expander (or an equivalent area) was clicked. - bool is_expandable = item is ExpandableItem; - bool is_category = is_expandable && data_model.is_category (item, null, path); - - if (event.type == Gdk.EventType.BUTTON_PRESS) { - if (is_expandable) { - // Checking for secondary_expander_cell is not necessary because the entire row - // serves for this purpose when the item is a category or when the item is a - // normal expandable item that is not selectable (special care is taken to - // not break the activatable/action icons for such cases). - // The expander only works like a visual indicator for these items. - unselectable_item_clicked = is_category - || (!item.selectable && !over_cell (column, path, activatable_cell, cell_x)); - - if (!unselectable_item_clicked - && over_primary_expander (column, path, cell_x) - && toggle_expansion (item as ExpandableItem)) - return true; - } - } else if ( - event.type == Gdk.EventType.2BUTTON_PRESS - && !is_category // Main categories are *not* editable - && item.editable - && item.selectable - && over_cell (column, path, text_cell, cell_x) - && start_editing_item (item) - ) { - // The user double-clicked over the text cell, and editing started successfully. - return true; - } - } - } - } - - return base.button_press_event (event); - } - - private bool over_primary_expander (Gtk.TreeViewColumn col, Gtk.TreePath path, int x) { - Gtk.TreeIter iter; - if (!model.get_iter (out iter, path)) - return false; - - // Call the cell-data function and make it assign the proper visibility state to the cell - expander_cell_data_func (col, primary_expander_cell, model, iter); - - if (!primary_expander_cell.visible) - return false; - - // We want to return false if the cell is not expandable (i.e. the arrow is hidden) - if (model.iter_n_children (iter) < 1) - return false; - - // Now that we're sure that the item is expandable, let's see if the user clicked - // over the expander area. We don't do so directly by querying the primary expander - // position because it's not fixed, yielding incorrect coordinates depending on whether - // a different area was re-drawn before this method was called. We know that the last - // spacer cell precedes (in a LTR fashion) the expander cell. Because the position - // of the spacer cell is fixed, we can safely query it. - int indentation_level = path.get_depth (); - var last_spacer_cell = spacer_cells[indentation_level]; - - if (last_spacer_cell != null) { - int cell_x, cell_width; - - if (col.cell_get_position (last_spacer_cell, out cell_x, out cell_width)) { - // Add a pixel so that the expander area is a bit wider - int expander_width = get_cell_width (primary_expander_cell) + 1; - - var dir = get_direction (); - if (dir == Gtk.TextDirection.NONE) { - dir = Gtk.Widget.get_default_direction (); - } - - if (dir == Gtk.TextDirection.LTR) { - int indentation_offset = cell_x + cell_width; - return x >= indentation_offset && x <= indentation_offset + expander_width; - } - - return x <= cell_x && x >= cell_x - expander_width; - } - } - - return false; - } - - private bool over_cell (Gtk.TreeViewColumn col, Gtk.TreePath path, Gtk.CellRenderer cell, int x) { - int cell_x, cell_width; - bool found = col.cell_get_position (cell, out cell_x, out cell_width); - return found && x > cell_x && x < cell_x + cell_width; - } - - private int get_cell_width (Gtk.CellRenderer cell_renderer) { - Gtk.Requisition min_req; - cell_renderer.get_preferred_size (this, out min_req, null); - return min_req.width; - } - - public override bool popup_menu () { - return popup_context_menu (null, null); - } - - private bool popup_context_menu (Item? item, Gdk.EventButton? event) { - if (item == null) - item = selected_item; - - if (item != null) { - var menu = item.get_context_menu (); - if (menu != null) { - menu.attach_widget = this; - menu.popup_at_pointer (event); - if (event == null) { - menu.select_first (false); - } - - return true; - } - } - - return false; - } - - private static Item? get_item_from_model (Gtk.TreeModel model, Gtk.TreeIter iter) { - var data_model = model as DataModel; - assert (data_model != null); - return data_model.get_item (iter); - } - - private static void spacer_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var spacer = renderer as CellRendererSpacer; - assert (spacer != null); - assert (spacer.level > 0); - - var path = model.get_path (iter); - - int level = -1; - if (path != null) - level = path.get_depth (); - - renderer.visible = spacer.level <= level; - } - - private void name_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var text_renderer = renderer as Gtk.CellRendererText; - assert (text_renderer != null); - - var text = new StringBuilder (); - var weight = Pango.Weight.NORMAL; - bool use_markup = false; - - var item = get_item_from_model (model, iter); - if (item != null) { - if (item.markup != null) { - text.append (item.markup); - use_markup = true; - } else { - text.append (item.name); - } - - if (data_model.is_category (item, iter)) - weight = Pango.Weight.BOLD; - } - - text_renderer.weight = weight; - if (use_markup) { - text_renderer.markup = text.str; - } else { - text_renderer.text = text.str; - } - } - - private void badge_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var badge_renderer = renderer as CellRendererBadge; - assert (badge_renderer != null); - - string text = ""; - bool visible = false; - - var item = get_item_from_model (model, iter); - if (item != null) { - // Badges are not displayed for main categories - visible = !data_model.is_category (item, iter) - && item.badge != null - && item.badge.strip () != ""; - - if (visible) - text = item.badge; - } - - badge_renderer.visible = visible; - badge_renderer.text = text; - } - - private void icon_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, Gtk.TreeIter iter - ) { - var icon_renderer = renderer as CellRendererIcon; - assert (icon_renderer != null); - - bool visible = false; - Icon? icon = null; - - var item = get_item_from_model (model, iter); - if (item != null) { - // Icons are not displayed for main categories - visible = !data_model.is_category (item, iter); - - if (visible) { - if (icon_renderer == icon_cell) - icon = item.icon; - else if (icon_renderer == activatable_cell) - icon = item.activatable; - else - assert_not_reached (); - } - } - - visible = visible && icon != null; - - icon_renderer.visible = visible; - icon_renderer.gicon = visible ? icon : null; - } - - /** - * Controls expander visibility. - */ - private void expander_cell_data_func ( - Gtk.CellLayout layout, - Gtk.CellRenderer renderer, - Gtk.TreeModel model, - Gtk.TreeIter iter - ) { - var item = get_item_from_model (model, iter); - if (item != null) { - // Gtk.CellRenderer.is_expander takes into account whether the item has children or not. - // The tree-view checks for that and sets this property for us. It also sets - // Gtk.CellRenderer.is_expanded, and thus we don't need to check for that either. - var expandable_item = item as ExpandableItem; - if (expandable_item != null) - renderer.is_expander = renderer.is_expander && expandable_item.collapsible; - } - - if (renderer == primary_expander_cell) - renderer.visible = !data_model.is_iter_at_root_level (iter); - else if (renderer == secondary_expander_cell) - renderer.visible = data_model.is_category (item, iter); - else - assert_not_reached (); - } - } - - - - /** - * Emitted when the source list selection changes. - * - * @param item Selected item; //null// if nothing is selected. - * @since 0.2 - */ - public virtual signal void item_selected (Item? item) { } - - /** - * A {@link Granite.Widgets.SourceList.VisibleFunc} should return true if the item should be - * visible; false otherwise. If //item//'s {@link Granite.Widgets.SourceList.Item.visible} - * property is set to //false//, then it won't be displayed even if this method returns //true//. - * - * It is important to note that the method ''must not modify any property of //item//''. - * Doing so would result in an infinite loop, freezing the application's user interface. - * This happens because the source list invokes this method to "filter" an item after - * any of its properties changes, so by modifying a property this method would be invoking - * itself again. - * - * For most use cases, modifying the {@link Granite.Widgets.SourceList.Item.visible} property is enough. - * - * The advantage of using this method is that its nature is non-destructive, and the - * changes it makes can be easily reverted (see {@link Granite.Widgets.SourceList.refilter}). - * - * @param item Item to be checked. - * @return Whether //item// should be visible or not. - * @since 0.2 - */ - public delegate bool VisibleFunc (Item item); - - /** - * Root-level expandable item. - * - * This item contains the first-level source list items. It //only serves as an item container//. - * It is used to add and remove items to/from the widget. - * - * Internally, it allows the source list to connect to its {@link Granite.Widgets.SourceList.ExpandableItem.child_added} - * and {@link Granite.Widgets.SourceList.ExpandableItem.child_removed} signals in order to monitor - * new children additions/removals. - * - * @since 0.2 - */ - public ExpandableItem root { - get { return data_model.root; } - set { data_model.root = value; } - } - - /** - * The current selected item. - * - * Setting it to //null// un-selects the previously selected item, if there was any. - * {@link Granite.Widgets.SourceList.ExpandableItem.expand_with_parents} is called on the - * item's parent to make sure it's possible to select it. - * - * @since 0.2 - */ - public Item? selected { - get { return tree.selected_item; } - set { - if (value != null && value.parent != null) - value.parent.expand_with_parents (); - tree.selected_item = value; - } - } - - /** - * Text ellipsize mode. - * - * @since 0.2 - */ - private Pango.EllipsizeMode ellipsize_mode { - get { return tree.ellipsize_mode; } - set { tree.ellipsize_mode = value; } - } - - /** - * Whether an item is being edited. - * - * @see Granite.Widgets.SourceList.start_editing_item - * @since 0.2 - */ - private bool editing { - get { return tree.editing; } - } - - private Tree tree; - private DataModel data_model = new DataModel (); - - /** - * Creates a new {@link Granite.Widgets.SourceList}. - * - * @return A new {@link Granite.Widgets.SourceList}. - * @since 0.2 - */ - public SourceList (ExpandableItem root = new ExpandableItem ()) { - this.root = root; - } - - construct { - tree = new Tree (data_model); - - set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); - add (tree); - show_all (); - - tree.item_selected.connect ((item) => item_selected (item)); - } - - /** - * Checks whether //item// is part of the source list. - * - * @param item The item to query. - * @return //true// if the item belongs to the source list; //false// otherwise. - * @since 0.2 - */ - public bool has_item (Item item) { - return data_model.has_item (item); - } - - /** - * Sets the method used for filtering out items. - * - * @param visible_func The method to use for filtering items. - * @param refilter Whether to call {@link Granite.Widgets.SourceList.refilter} using the new function. - * @see Granite.Widgets.SourceList.VisibleFunc - * @see Granite.Widgets.SourceList.refilter - * @since 0.2 - */ - public void set_filter_func (VisibleFunc? visible_func, bool refilter) { - data_model.set_filter_func (visible_func); - if (refilter) - this.refilter (); - } - - /** - * Applies the filter method set by {@link Granite.Widgets.SourceList.set_filter_func} - * to all the items that are part of the current tree. - * - * @see Granite.Widgets.SourceList.VisibleFunc - * @see Granite.Widgets.SourceList.set_filter_func - * @since 0.2 - */ - private void refilter () { - data_model.refilter (); - } -} diff --git a/src/VirtualizingListBox/VirtualizingListBox.vala b/src/VirtualizingListBox/VirtualizingListBox.vala deleted file mode 100644 index e1c5ee085..000000000 --- a/src/VirtualizingListBox/VirtualizingListBox.vala +++ /dev/null @@ -1,881 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2018 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: David Hewitt - */ - -public class VirtualizingListBox : Gtk.Container, Gtk.Scrollable { - public delegate VirtualizingListBoxRow RowFactoryMethod (GLib.Object item, VirtualizingListBoxRow? old_widget); - - public RowFactoryMethod factory_func; - - public signal void row_activated (GLib.Object row); - public signal void row_selected (GLib.Object row); - public signal void selected_rows_changed (); - - private VirtualizingListBoxModel? _model; - public VirtualizingListBoxModel? model { - get { - return _model; - } - set { - if (_model != null) { - _model.items_changed.disconnect (on_items_changed); - } - - _model = value; - _model.items_changed.connect (on_items_changed); - queue_resize (); - } - } - - private Gtk.Adjustment? _vadjustment; - public Gtk.Adjustment? vadjustment { - set { - if (_vadjustment != null) { - _vadjustment.value_changed.disconnect (on_adjustment_value_changed); - _vadjustment.notify["page-size"].disconnect (on_adjustment_page_size_changed); - } - - _vadjustment = value; - if (_vadjustment != null) { - _vadjustment.value_changed.connect (on_adjustment_value_changed); - _vadjustment.notify["page-size"].connect (on_adjustment_page_size_changed); - configure_adjustment (); - } - } - get { - return _vadjustment; - } - } - - private int bin_y { - get { - int y = 0; - if (vadjustment != null) { - y = -(int)vadjustment.value; //vala-lint=space-before-paren - } - - return y + (int)bin_y_diff; - } - } - - private bool bin_window_full { - get { - int bin_height = 0; - if (get_realized ()) { - bin_height = bin_window.get_height (); - } - - var widget_height = get_allocated_height (); - return (bin_y + bin_height > widget_height) || (shown_to - shown_from == model.get_n_items ()); - } - } - - public VirtualizingListBoxRow? selected_row_widget { - get { - var item = selected_row; - - foreach (var child in current_widgets) { - if (child.model_item == item) { - return (VirtualizingListBoxRow)child; - } - } - - return null; - } - } - - public Gtk.Adjustment hadjustment { get; set; } - public Gtk.ScrollablePolicy hscroll_policy { get; set; } - public Gtk.ScrollablePolicy vscroll_policy { get; set; } - public bool activate_on_single_click { get; set; } - public Gtk.SelectionMode selection_mode { get; set; default = Gtk.SelectionMode.SINGLE; } - private double bin_y_diff { get; private set; } - public GLib.Object selected_row { get; private set; } - - private Gee.ArrayList current_widgets = new Gee.ArrayList (); - private Gee.ArrayList recycled_widgets = new Gee.ArrayList (); - private Gdk.Window bin_window; - private uint shown_to; - private uint shown_from; - private bool block; - private int last_valid_widget_height = 1; - private VirtualizingListBoxRow? active_row; - private Gtk.GestureMultiPress multipress; - - static construct { - set_css_name ("list"); - } - - construct { - multipress = new Gtk.GestureMultiPress (this); - multipress.set_propagation_phase (Gtk.PropagationPhase.BUBBLE); - multipress.touch_only = false; - multipress.button = Gdk.BUTTON_PRIMARY; - multipress.pressed.connect (on_multipress_pressed); - multipress.released.connect (on_multipress_released); - } - - public override void realize () { - set_realized (true); - Gtk.Allocation allocation; - get_allocation (out allocation); - - var attr = Gdk.WindowAttr (); - attr.x = allocation.x; - attr.y = allocation.y; - attr.width = allocation.width; - attr.height = allocation.height; - attr.window_type = Gdk.WindowType.CHILD; - attr.event_mask = Gdk.EventMask.ALL_EVENTS_MASK; - attr.wclass = Gdk.WindowWindowClass.INPUT_OUTPUT; - attr.visual = get_visual (); - - Gdk.WindowAttributesType attr_types; - attr_types = Gdk.WindowAttributesType.X | Gdk.WindowAttributesType.Y | Gdk.WindowAttributesType.VISUAL; - - var window = new Gdk.Window (get_parent_window (), attr, attr_types); - - set_window (window); - register_window (window); - - attr.height = 1; - bin_window = new Gdk.Window (window, attr, attr_types); - register_window (bin_window); - bin_window.show (); - } - - public override void size_allocate (Gtk.Allocation allocation) { - bool height_changed = allocation.height != get_allocated_height (); - bool width_changed = allocation.width != get_allocated_width (); - set_allocation (allocation); - position_children (); - - if (get_realized ()) { - get_window ().move_resize (allocation.x, - allocation.y, - allocation.width, - allocation.height); - update_bin_window (); - } - - if (vadjustment != null && height_changed || width_changed) { - configure_adjustment (); - } - - if (height_changed || width_changed) { - ensure_visible_widgets (); - } - } - - public override void map () { - base.map (); - ensure_visible_widgets (); - } - - public override void remove (Gtk.Widget w) { - assert (w.get_parent () == this); - } - - public override void forall_internal (bool include_internals, Gtk.Callback callback) { - foreach (var child in current_widgets) { - callback (child); - } - } - - public override GLib.Type child_type () { - return typeof (VirtualizingListBoxRow); - } - - private VirtualizingListBoxRow? get_widget (uint index) { - var item = model.get_object (index); - if (item == null) { - return null; - } - - VirtualizingListBoxRow? old_widget = null; - if (recycled_widgets.size > 0) { - old_widget = recycled_widgets[recycled_widgets.size - 1]; - recycled_widgets.remove (old_widget); - } - - VirtualizingListBoxRow new_widget = factory_func (item, old_widget); - if (model.get_item_selected (item)) { - new_widget.set_state_flags (Gtk.StateFlags.SELECTED, false); - } else { - new_widget.unset_state_flags (Gtk.StateFlags.SELECTED); - } - - new_widget.model_item = item; - new_widget.show (); - - return new_widget; - } - - private void on_items_changed (uint position, uint removed, uint added) { - if (position >= shown_to && bin_window_full) { - if (vadjustment == null) { - queue_resize (); - } else { - configure_adjustment (); - } - - return; - } - - remove_all_widgets (); - shown_to = shown_from; - update_bin_window (); - ensure_visible_widgets (true); - - if (vadjustment == null) { - queue_resize (); - } - } - - private inline int widget_y (int index) { - int y = 0; - for (int i = 0; i < index; i ++) { - y += get_widget_height (current_widgets[i]); - } - - return y; - } - - private int get_widget_height (Gtk.Widget w) { - int min; - w.get_preferred_height_for_width (get_allocated_width (), out min, null); - return min; - } - - private void position_children () { - Gtk.Allocation allocation; - Gtk.Allocation child_allocation = {0}; - - get_allocation (out allocation); - - int y = 0; - if (vadjustment != null) { - y = allocation.y; - } - - child_allocation.x = 0; - if (allocation.width > 0) { - child_allocation.width = allocation.width; - } else { - child_allocation.width = 1; - } - - foreach (var child in current_widgets) { - child.get_preferred_height_for_width (get_allocated_width (), out child_allocation.height, null); - child.get_preferred_width_for_height (child_allocation.height, out child_allocation.width, null); - child_allocation.width = int.max (child_allocation.width, get_allocated_width ()); - child_allocation.y = y; - child.size_allocate (child_allocation); - - y += child_allocation.height; - } - } - - private void update_bin_window (int new_bin_height = -1) { - Gtk.Allocation allocation; - get_allocation (out allocation); - - var h = (new_bin_height == -1 ? 0 : new_bin_height); - - if (new_bin_height == -1) { - foreach (var w in current_widgets) { - h += get_widget_height (w); - } - } - - if (h == 0) { - h = 1; - } - - if (h != bin_window.get_height () || allocation.width != bin_window.get_width ()) { - bin_window.move_resize (0, bin_y, allocation.width, h); - } else { - bin_window.move (0, bin_y); - } - } - - private void remove_all_widgets () { - foreach (var w in current_widgets) { - w.unparent (); - } - - recycled_widgets.add_all (current_widgets); - current_widgets.clear (); - } - - private void remove_child_internal (VirtualizingListBoxRow widget) { - current_widgets.remove (widget); - widget.set_state_flags (Gtk.StateFlags.NORMAL, true); - widget.unparent (); - recycled_widgets.add (widget); - } - - private void on_adjustment_value_changed () { - ensure_visible_widgets (); - } - - private void on_adjustment_page_size_changed () { - if (!get_mapped ()) { - return; - } - - double max_value = vadjustment.upper - vadjustment.page_size; - - if (vadjustment.value > max_value) { - set_value (max_value); - } - - configure_adjustment (); - } - - private void insert_child_internal (VirtualizingListBoxRow widget, int index) { - widget.set_parent_window (bin_window); - widget.set_parent (this); - current_widgets.insert (index, widget); - } - - private bool remove_top_widgets (ref int bin_height) { - bool removed = false; - for (int i = 0; i < current_widgets.size; i++) { - var w = current_widgets[i]; - int w_height = get_widget_height (w); - if (bin_y + widget_y (i) + w_height < 0) { - bin_y_diff += w_height; - bin_height -= w_height; - remove_child_internal (w); - shown_from++; - removed = true; - } else { - break; - } - } - - return removed; - } - - private bool insert_top_widgets (ref int bin_height) { - bool added = false; - while (shown_from > 0 && bin_y >= 0) { - shown_from--; - var new_widget = get_widget (shown_from); - if (new_widget == null) { - continue; - } - - insert_child_internal (new_widget, 0); - var min = get_widget_height (new_widget); - - bin_y_diff -= min; - bin_height += min; - added = true; - } - - if (bin_y > 0) { - bin_y_diff = 0; - block = true; - set_value (0.0); - block = false; - } - - return added; - } - - private bool remove_bottom_widgets (ref int bin_height) { - for (int i = current_widgets.size - 1; i >= 0; i--) { - var w = current_widgets[i]; - - int widget_y = bin_y + widget_y (i); - if (widget_y > get_allocated_height ()) { - int w_height = get_widget_height (w); - remove_child_internal (w); - bin_height -= w_height; - shown_to--; - } else { - break; - } - } - - return false; - } - - private bool insert_bottom_widgets (ref int bin_height) { - bool added = false; - while (bin_y + bin_height <= get_allocated_height () && shown_to < model.get_n_items ()) { - var new_widget = get_widget (shown_to); - if (new_widget == null) { - shown_to++; - continue; - } - - insert_child_internal (new_widget, current_widgets.size); - - int min = get_widget_height (new_widget); - bin_height += min; - added = true; - shown_to ++; - } - - return added; - } - - private void ensure_visible_widgets (bool model_changed = false) { - if (!get_mapped () || model == null || block) { - return; - } - - var widget_height = get_allocated_height (); - var bin_height = bin_window.get_height (); - if (bin_height == 1) { - bin_height = 0; - } - - if (bin_y + bin_height < 0 || bin_y >= widget_height) { - int estimated_widget_height = estimated_widget_height (); - - remove_all_widgets (); - bin_height = 0; - - double percentage = vadjustment.value / vadjustment.upper; - uint top_widget_index = (uint)(model.get_n_items () * percentage); - - if (top_widget_index > model.get_n_items ()) { - shown_to = model.get_n_items (); - shown_from = model.get_n_items (); - bin_y_diff = vadjustment.value + vadjustment.page_size; - } else { - shown_from = top_widget_index; - shown_to = top_widget_index; - bin_y_diff = top_widget_index * estimated_widget_height; - } - } - - var top_removed = remove_top_widgets (ref bin_height); - var top_added = insert_top_widgets (ref bin_height); - var bottom_removed = remove_bottom_widgets (ref bin_height); - var bottom_added = insert_bottom_widgets (ref bin_height); - - var widgets_changed = top_removed || top_added || bottom_removed || bottom_added || model_changed; - - if (vadjustment != null && widgets_changed) { - uint top_part; - uint widget_part; - uint bottom_part; - - uint new_upper = estimated_list_height (out top_part, out bottom_part, out widget_part); - - if (new_upper > _vadjustment.upper) { - bin_y_diff = double.max (top_part, vadjustment.value); - } else { - bin_y_diff = double.min (top_part, vadjustment.value); - } - - configure_adjustment (); - - set_value (bin_y_diff - bin_y); - if (vadjustment.value < bin_y_diff) { - set_value (bin_y_diff); - } - - if (bin_y > 0) { - bin_y_diff = vadjustment.value; - } - } - - configure_adjustment (); - update_bin_window (bin_height); - position_children (); - queue_draw (); - } - - private int estimated_widget_height () { - int average_widget_height = 0; - int used_widgets = 0; - - foreach (var w in current_widgets) { - if (w.visible) { - average_widget_height += get_widget_height (w); - used_widgets ++; - } - } - - if (used_widgets > 0) { - average_widget_height /= used_widgets; - } else { - average_widget_height = last_valid_widget_height; - } - - last_valid_widget_height = average_widget_height; - - return average_widget_height; - } - - private void configure_adjustment () { - int widget_height = get_allocated_height (); - uint list_height = estimated_list_height (); - - if ((int)vadjustment.upper != uint.max (list_height, widget_height)) { - vadjustment.upper = uint.max (list_height, widget_height); - } else if (list_height == 0) { - vadjustment.upper = widget_height; - } - - if ((int)vadjustment.page_size != widget_height) { - vadjustment.page_size = widget_height; - } - - if (vadjustment.value > vadjustment.upper - vadjustment.page_size) { - double v = vadjustment.upper - vadjustment.page_size; - set_value (v); - } - } - - private void set_value (double v) { - if (v == vadjustment.value) - return; - - block = true; - vadjustment.value = v; - block = false; - } - - private uint estimated_list_height (out uint top = null, out uint bottom = null, out uint visible_widgets = null) { - if (model == null) { - top = 0; - bottom = 0; - visible_widgets = 0; - return 0; - } - - int widget_height = estimated_widget_height (); - uint top_widgets = shown_from; - uint bottom_widgets = model.get_n_items () - shown_to; - - int exact_height = 0; - foreach (var w in current_widgets) { - int h = get_widget_height (w); - exact_height += h; - } - - top = top_widgets * widget_height; - bottom = bottom_widgets * widget_height; - visible_widgets = exact_height; - - uint h = top + bottom + visible_widgets; - return h; - } - - public unowned VirtualizingListBoxRow? get_row_at_y (int y) { - Gtk.Allocation alloc; - foreach (var row in current_widgets) { - row.get_allocation (out alloc); - if (y >= alloc.y + bin_y && y <= alloc.y + bin_y + alloc.height) { - unowned VirtualizingListBoxRow return_value = row; - return return_value; - } - } - - return null; - } - - private void on_multipress_pressed (int n_press, double x, double y) { - active_row = null; - var row = get_row_at_y ((int)y); - if (row != null && row.sensitive) { - active_row = row; - row.set_state_flags (Gtk.StateFlags.ACTIVE, false); - - if (n_press == 2 && !activate_on_single_click) { - row_activated (row.model_item); - } - } - } - - private void get_current_selection_modifiers (out bool modify, out bool extend) { - Gdk.ModifierType state; - Gdk.ModifierType mask; - - modify = false; - extend = false; - - if (Gtk.get_current_event_state (out state)) { - mask = get_modifier_mask (Gdk.ModifierIntent.MODIFY_SELECTION); - if ((state & mask) == mask) { - modify = true; - } - - mask = get_modifier_mask (Gdk.ModifierIntent.EXTEND_SELECTION); - if ((state & mask) == mask) { - extend = true; - } - } - } - - private void on_multipress_released (int n_press, double x, double y) { - if (active_row != null) { - var focus_on_click = active_row.focus_on_click; - active_row.unset_state_flags (Gtk.StateFlags.ACTIVE); - - if (n_press == 1 && activate_on_single_click) { - select_and_activate (active_row, focus_on_click); - } else { - bool modify, extend; - get_current_selection_modifiers (out modify, out extend); - var sequence = multipress.get_current_sequence (); - var event = multipress.get_last_event (sequence); - var source = event.get_source_device ().get_source (); - - if (source == Gdk.InputSource.TOUCHSCREEN) { - modify = !modify; - } - - update_selection (active_row, modify, extend, focus_on_click); - } - } - } - - private void update_selection (VirtualizingListBoxRow row, bool modify, bool extend, bool grab_cursor = true) { - update_cursor (row.model_item, grab_cursor); - - if (selection_mode == Gtk.SelectionMode.NONE || !row.selectable) { - return; - } - - if (selection_mode == Gtk.SelectionMode.BROWSE) { - select_row (row); - } else if (selection_mode == Gtk.SelectionMode.SINGLE) { - var was_selected = model.get_item_selected (row.model_item); - unselect_all_internal (); - var select = modify ? !was_selected : true; - model.set_item_selected (row.model_item, select); - selected_row = select ? row.model_item : null; - if (select) { - row.set_state_flags (Gtk.StateFlags.SELECTED, false); - } else { - row.unset_state_flags (Gtk.StateFlags.SELECTED); - } - - row_selected (selected_row); - } else { - if (extend) { - var selected = selected_row; - unselect_all_internal (); - if (selected == null) { - select_row (row); - } else { - select_all_between (selected, row.model_item, false); - } - } else { - if (modify) { - var selected = model.get_item_selected (row.model_item); - if (selected) { - row.unset_state_flags (Gtk.StateFlags.SELECTED); - } else { - row.set_state_flags (Gtk.StateFlags.SELECTED, false); - } - - model.set_item_selected (row.model_item, !selected); - } else { - unselect_all_internal (); - select_row (row); - } - } - } - } - - private void select_all_between (GLib.Object from, GLib.Object to, bool modify) { - var items = model.get_items_between (from, to); - foreach (var item in items) { - model.set_item_selected (item, true); - } - - foreach (VirtualizingListBoxRow row in current_widgets) { - if (row.model_item in items) { - row.set_state_flags (Gtk.StateFlags.SELECTED, false); - } - } - } - - public void select_row_at_index (int index) { - var row = ensure_index_visible (index); - - if (row != null) { - select_and_activate (row); - } - } - - private void select_and_activate (VirtualizingListBoxRow row, bool grab_focus = true) { - select_row (row); - update_cursor (row.model_item, grab_focus); - row_activated (row.model_item); - } - - public void update_cursor (GLib.Object item, bool grab_focus) { - var row = ensure_index_visible (model.get_index_of (item)); - if (row != null && grab_focus) { - row.grab_focus (); - } - } - - private VirtualizingListBoxRow? ensure_index_visible (int index) { - if (index < 0) { - return null; - } - - var n_items = model.get_n_items (); - if (n_items == 0) { - return null; - } - - var index_max = n_items - 1; - if (index > index_max) { - return null; - } - - if (index == 0) { - set_value (0.0); - ensure_visible_widgets (); - foreach (VirtualizingListBoxRow row in current_widgets) { - if (index == model.get_index_of (row.model_item)) { - return row; - } - } - } - - if (index == index_max) { - set_value (vadjustment.upper); - ensure_visible_widgets (); - foreach (VirtualizingListBoxRow row in current_widgets) { - if (index == model.get_index_of (row.model_item)) { - return row; - } - } - } - - while (index <= shown_from) { - vadjustment.value--; - ensure_visible_widgets (); - } - - while (index + 1 >= shown_to) { - vadjustment.value++; - ensure_visible_widgets (); - } - - foreach (VirtualizingListBoxRow row in current_widgets) { - if (index == model.get_index_of (row.model_item)) { - return row; - } - } - - return null; - } - - public void select_row (VirtualizingListBoxRow row) { - if (model.get_item_selected (row) || selection_mode == Gtk.SelectionMode.NONE) { - return; - } - - if (selection_mode != Gtk.SelectionMode.MULTIPLE) { - unselect_all_internal (); - } - - model.set_item_selected (row.model_item, true); - row.set_state_flags (Gtk.StateFlags.SELECTED, false); - selected_row = row.model_item; - - row_selected (row.model_item); - selected_rows_changed (); - } - - private bool unselect_all_internal () { - if (selection_mode == Gtk.SelectionMode.NONE) { - return false; - } - - foreach (var row in current_widgets) { - row.unset_state_flags (Gtk.StateFlags.SELECTED); - } - - model.unselect_all (); - selected_row = null; - - return true; - } - - public override bool focus (Gtk.DirectionType direction) { - var focus_child = get_focus_child () as VirtualizingListBoxRow; - int next_focus_index = -1; - - if (focus_child != null && focus_child.model_item != null) { - if (focus_child.child_focus (direction)) { - return true; - } - - if (direction == Gtk.DirectionType.UP || direction == Gtk.DirectionType.TAB_BACKWARD) { - next_focus_index = model.get_index_of_item_before (focus_child.model_item); - } else if (direction == Gtk.DirectionType.DOWN || direction == Gtk.DirectionType.TAB_FORWARD) { - next_focus_index = model.get_index_of_item_after (focus_child.model_item); - } - } else { - if (direction == Gtk.DirectionType.UP || direction == Gtk.DirectionType.TAB_BACKWARD) { - next_focus_index = model.get_index_of (focus_child.model_item); - if (next_focus_index == -1) { - next_focus_index = (int)model.get_n_items () - 1; - } - } else { - next_focus_index = model.get_index_of (focus_child); - if (next_focus_index == -1) { - next_focus_index = 0; - } - } - } - - if (next_focus_index == -1) { - if (keynav_failed (direction)) { - return true; - } - - return false; - } - - var widget = ensure_index_visible (next_focus_index); - if (widget != null) { - update_selection (widget, false, false); - return true; - } - - return false; - } - - public bool get_border (out Gtk.Border border) { - border = Gtk.Border (); - return false; - } - - public Gee.HashSet get_selected_rows () { - return model.get_selected_rows (); - } -} diff --git a/src/VirtualizingListBox/VirtualizingListBoxModel.vala b/src/VirtualizingListBox/VirtualizingListBoxModel.vala deleted file mode 100644 index 61e56a18c..000000000 --- a/src/VirtualizingListBox/VirtualizingListBoxModel.vala +++ /dev/null @@ -1,145 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2018 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: David Hewitt - */ - -public abstract class VirtualizingListBoxModel : GLib.ListModel, GLib.Object { - private Gee.HashSet selected_rows = new Gee.HashSet (); - - public GLib.Type get_item_type () { - return typeof (GLib.Object); - } - - public abstract uint get_n_items (); - public abstract GLib.Object? get_item (uint index); - public abstract GLib.Object? get_item_unfiltered (uint index); - - public void unselect_all () { - selected_rows.clear (); - } - - public void set_item_selected (GLib.Object item, bool selected) { - if (!selected) { - selected_rows.remove (item); - } else { - selected_rows.add (item); - } - } - - public bool get_item_selected (GLib.Object item) { - return selected_rows.contains (item); - } - - public Gee.ArrayList get_items_between (GLib.Object from, GLib.Object to) { - var items = new Gee.ArrayList (); - var start_found = false; - var ignore_next_break = false; - var length = get_n_items (); - for (int i = 0; i < length; i++) { - var item = get_item (i); - if ((item == from || item == to) && !start_found) { - start_found = true; - ignore_next_break = true; - } else if (!start_found) { - continue; - } - - if (item != null) { - items.add (item); - } - - if ((item == to || item == from) && !ignore_next_break) { - break; - } - - ignore_next_break = false; - } - - return items; - } - - public int get_index_of (GLib.Object? item) { - if (item == null) { - return -1; - } - - var length = get_n_items (); - for (int i = 0; i < length; i++) { - if (item == get_item (i)) { - return i; - } - } - - return -1; - } - - public int get_index_of_unfiltered (GLib.Object? item) { - if (item == null) { - return -1; - } - - var length = get_n_items (); - for (int i = 0; i < length; i++) { - if (item == get_item_unfiltered (i)) { - return i; - } - } - - return -1; - } - - public int get_index_of_item_before (GLib.Object item) { - if (item == get_item (0)) { - return -1; - } - - var length = get_n_items (); - for (int i = 1; i < length; i++) { - if (get_item (i) == item) { - if (get_item (i - 1) != null) { - return i - 1; - } - } - } - - return -1; - } - - public int get_index_of_item_after (GLib.Object item) { - if (item == get_item (get_n_items () - 1)) { - return -1; - } - - var length = get_n_items (); - for (int i = 0; i < length - 1; i++) { - if (get_item (i) == item) { - if (get_item (i + 1) != null) { - return i + 1; - } - } - } - - return -1; - } - - public Gee.HashSet get_selected_rows () { - return selected_rows; - } -} diff --git a/src/VirtualizingListBox/VirtualizingListBoxRow.vala b/src/VirtualizingListBox/VirtualizingListBoxRow.vala deleted file mode 100644 index 8ace8ebeb..000000000 --- a/src/VirtualizingListBox/VirtualizingListBoxRow.vala +++ /dev/null @@ -1,48 +0,0 @@ -// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- -/*- - * Copyright (c) 2018 elementary LLC. (https://elementary.io) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Authored by: David Hewitt - */ - -public class VirtualizingListBoxRow : Gtk.Bin { - public bool selectable { get; set; default = true; } - public weak GLib.Object model_item { get; set; } - - static construct { - set_css_name ("row"); - } - - construct { - can_focus = true; - set_redraw_on_allocate (true); - - get_style_context ().add_class ("activatable"); - } - - public override bool draw (Cairo.Context ct) { - var sc = this.get_style_context (); - Gtk.Allocation alloc; - this.get_allocation (out alloc); - - sc.render_background (ct, 0, 0, alloc.width, alloc.height); - sc.render_frame (ct, 0, 0, alloc.width, alloc.height); - - return base.draw (ct); - } -} diff --git a/src/WebView.vala b/src/WebView.vala index e30e9363c..532aaf819 100644 --- a/src/WebView.vala +++ b/src/WebView.vala @@ -26,12 +26,9 @@ public class Mail.WebView : WebKit.WebView { public signal void selection_changed (); public signal void load_finished (); - public bool body_html_changed { get; private set; default = false; } - private const string INTERNAL_URL_BODY = "elementary-mail:body"; private const string SERVER_BUS_NAME = "io.elementary.mail.WebViewServer"; - private int preferred_height = 0; private Gee.Map internal_resources; private bool loaded = false; @@ -42,8 +39,8 @@ public class Mail.WebView : WebKit.WebView { static construct { unowned WebKit.WebContext context = WebKit.WebContext.get_default (); unowned string? webkit_extension_path_env = Environment.get_variable ("WEBKIT_EXTENSION_PATH"); - context.set_web_extensions_directory (webkit_extension_path_env ?? WEBKIT_EXTENSION_PATH); - context.set_sandbox_enabled (true); + context.set_web_process_extensions_directory (webkit_extension_path_env ?? WEBKIT_EXTENSION_PATH); + // context.set_sandbox_enabled (true); context.register_uri_scheme ("cid", (req) => { WebView? view = req.get_web_view () as WebView; @@ -55,16 +52,13 @@ public class Mail.WebView : WebKit.WebView { construct { cancellable = new GLib.Cancellable (); - expand = true; + vexpand = true; + hexpand = true; internal_resources = new Gee.HashMap (); decide_policy.connect (on_decide_policy); load_changed.connect (on_load_changed); - - key_release_event.connect (() => { - body_html_changed = true; - }); } public WebView () { @@ -73,12 +67,12 @@ public class Mail.WebView : WebKit.WebView { setts.enable_fullscreen = false; setts.enable_html5_database = false; setts.enable_html5_local_storage = false; - setts.enable_java = false; + // setts.enable_java = false; setts.enable_javascript = false; setts.enable_media_stream = false; setts.enable_offline_web_application_cache = false; setts.enable_page_cache = false; - setts.enable_plugins = false; + // setts.enable_plugins = false; Object (settings: setts); } @@ -93,8 +87,7 @@ public class Mail.WebView : WebKit.WebView { send_message_to_page.begin (message, cancellable, (obj, res) => { try { var response = send_message_to_page.end (res); - preferred_height = response.parameters.get_int32 (); - queue_resize (); + height_request = response.parameters.get_int32 (); //@TODO: Needs refinement: On a quick switch of message this doesn't update correctly } catch (Error e) { // We can cancel the operation if (!(e is GLib.IOError.CANCELLED)) { @@ -118,10 +111,6 @@ public class Mail.WebView : WebKit.WebView { } } - public override void get_preferred_height (out int minimum_height, out int natural_height) { - minimum_height = natural_height = preferred_height; - } - public new void load_html (string? body) { base.load_html (body, INTERNAL_URL_BODY); } @@ -184,7 +173,6 @@ public class Mail.WebView : WebKit.WebView { public void execute_editor_command (string command, string argument = "") { var message = new WebKit.UserMessage ("execute-editor-command", new Variant ("(ss)", command, argument)); send_message_to_page.begin (message, cancellable); - body_html_changed = true; } public async bool query_command_state (string command) { @@ -246,11 +234,7 @@ public class Mail.WebView : WebKit.WebView { } private bool handle_internal_response (WebKit.URISchemeRequest request) { -#if HAS_SOUP_3 string name = GLib.Uri.unescape_string (request.get_path ()); -#else - string name = Soup.URI.decode (request.get_path ()); -#endif InputStream? buf = this.internal_resources[name]; if (buf != null) { request.finish (buf, -1, null); diff --git a/src/WelcomeView.vala b/src/WelcomeView.vala index 4a168a469..3250e1077 100644 --- a/src/WelcomeView.vala +++ b/src/WelcomeView.vala @@ -20,10 +20,8 @@ public class Mail.WelcomeView : Gtk.Box { construct { - var headerbar = new Hdy.HeaderBar () { - show_close_button = true - }; - headerbar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + var headerbar = new Gtk.HeaderBar (); + headerbar.add_css_class (Granite.STYLE_CLASS_FLAT); var welcome_icon = new Gtk.Image () { icon_name = "io.elementary.mail", @@ -32,7 +30,7 @@ public class Mail.WelcomeView : Gtk.Box { pixel_size = 64 }; - var welcome_badge = new Gtk.Image.from_icon_name ("preferences-desktop-online-accounts", Gtk.IconSize.DIALOG) { + var welcome_badge = new Gtk.Image.from_icon_name ("preferences-desktop-online-accounts") { halign = valign = Gtk.Align.END, }; @@ -47,24 +45,25 @@ public class Mail.WelcomeView : Gtk.Box { wrap = true, xalign = 0 }; - welcome_title.get_style_context ().add_class (Granite.STYLE_CLASS_H1_LABEL); + welcome_title.add_css_class (Granite.STYLE_CLASS_H1_LABEL); var welcome_description = new Gtk.Label (_("Mail uses email accounts configured in System Settings.")) { max_width_chars = 70, wrap = true, xalign = 0 }; - welcome_description.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + welcome_description.add_css_class (Granite.STYLE_CLASS_H3_LABEL); var welcome_button = new Gtk.Button.with_label (_("Online Accounts…")) { margin_top = 24 }; - welcome_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + welcome_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); var grid = new Gtk.Grid () { column_spacing = 12, halign = valign = Gtk.Align.CENTER, - expand = true + hexpand = true, + vexpand = true }; grid.attach (welcome_overlay, 0, 0, 1, 2); grid.attach (welcome_title, 1, 0); @@ -72,19 +71,19 @@ public class Mail.WelcomeView : Gtk.Box { grid.attach (welcome_button, 1, 2); var main_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - main_box.add (headerbar); - main_box.add (grid); + main_box.append (headerbar); + main_box.append (grid); - var window_handle = new Hdy.WindowHandle () { - child = main_box + var window_handle = new Gtk.WindowHandle () { + child = main_box, + hexpand = vexpand = true }; - add (window_handle); - show_all (); + append (window_handle); welcome_button.clicked.connect (() => { try { - Gtk.show_uri_on_window ((Gtk.Window) get_toplevel (), "settings://accounts/online", Gdk.CURRENT_TIME); + Gtk.show_uri ((Gtk.Window) get_root (), "settings://accounts/online", Gdk.CURRENT_TIME); } catch (Error e) { critical (e.message); } diff --git a/src/meson.build b/src/meson.build index a4e503f90..cef29646c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,22 +15,15 @@ vala_files = files( 'ConversationList/ConversationListItem.vala', 'ConversationList/ConversationListStore.vala', 'Dialogs/InsertLinkDialog.vala', - 'FoldersView/AccountSavedState.vala', - 'FoldersView/AccountSourceItem.vala', - 'FoldersView/FoldersListView.vala', - 'FoldersView/FolderSourceItem.vala', - 'FoldersView/GroupedFolderSourceItem.vala', - 'FoldersView/SessionSourceItem.vala', + 'FolderList/FolderList.vala', + 'FolderList/FolderListItem.vala', + 'FolderList/AccountItemModel.vala', + 'FolderList/FolderItemModel.vala', + 'FolderList/SessionItemModel.vala', + 'FolderList/GroupedFolderItemModel.vala', 'MessageList/AttachmentButton.vala', - 'MessageList/GravatarIcon.vala', 'MessageList/MessageList.vala', - 'MessageList/MessageListItem.vala', - 'SourceList/CellRendererBadge.vala', - 'SourceList/CellRendererExpander.vala', - 'SourceList/SourceList.vala', - 'VirtualizingListBox/VirtualizingListBoxModel.vala', - 'VirtualizingListBox/VirtualizingListBoxRow.vala', - 'VirtualizingListBox/VirtualizingListBox.vala' + 'MessageList/MessageListItem.vala' ) executable( @@ -39,6 +32,6 @@ executable( config_file, asresources, dependencies: dependencies, - c_args: '-DWEBKIT_EXTENSION_PATH="' + webkit2_extension_path + '"', + c_args: '-DWEBKIT_EXTENSION_PATH="' + webkit_extension_path + '"', install: true ) diff --git a/webkit-extension/MailPage.vala b/webkit-extension/MailPage.vala index 08a798420..e0881cad8 100644 --- a/webkit-extension/MailPage.vala +++ b/webkit-extension/MailPage.vala @@ -116,7 +116,6 @@ public class Mail.Page : Object { private bool on_send_request (WebKit.WebPage page, WebKit.URIRequest request, WebKit.URIResponse? response) { bool should_load = false; -#if HAS_SOUP_3 GLib.Uri? uri = null; try { uri = GLib.Uri.parse (request.get_uri (), GLib.UriFlags.NONE); @@ -124,9 +123,6 @@ public class Mail.Page : Object { warning ("Could not parse uri: %s", e.message); return should_load; } -#else - Soup.URI? uri = new Soup.URI (request.get_uri ()); -#endif if (uri != null && uri.get_scheme () in ALLOWED_SCHEMES) { // Always load internal resources should_load = true; diff --git a/webkit-extension/Main.vala b/webkit-extension/Main.vala index 5ae3e1582..d5ffbf6f9 100644 --- a/webkit-extension/Main.vala +++ b/webkit-extension/Main.vala @@ -17,15 +17,15 @@ * Authored by: David Hewitt */ -namespace WebkitWebExtension { +namespace WebkitWebProcessExtension { private static void on_page_created (WebKit.WebPage page) { var mail_page = new Mail.Page (page); // Make so that the Mail.Page is destroyed at the same time of the WebKit.WebPage page.set_data ("elementary-mail-page", (owned) mail_page); } - [CCode (cname = "G_MODULE_EXPORT webkit_web_extension_initialize", instance_pos = -1)] - public void initialize (WebKit.WebExtension extension) { + [CCode (cname = "G_MODULE_EXPORT webkit_web_process_extension_initialize", instance_pos = -1)] + public void initialize (WebKit.WebProcessExtension extension) { extension.page_created.connect (on_page_created); } } diff --git a/webkit-extension/meson.build b/webkit-extension/meson.build index 40c0239c9..f06726493 100644 --- a/webkit-extension/meson.build +++ b/webkit-extension/meson.build @@ -7,6 +7,6 @@ shared_module('io.elementary.mail-webkit-extension', extension_files, dependencies: extension_dependencies, install: true, - install_dir: webkit2_extension_path + install_dir: webkit_extension_path )