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
)