Fix UI for libadwaita

This commit is contained in:
Marvin W 2023-01-24 18:57:04 +01:00
parent cc7db3b85f
commit e35df88d4a
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
15 changed files with 422 additions and 281 deletions

View file

@ -154,6 +154,7 @@ public interface ConversationItemWidgetInterface: Object {
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
public class MessageAction : Object { public class MessageAction : Object {
public string icon_name; public string icon_name;
public string? tooltip;
public Object? popover; public Object? popover;
public MessageActionEvoked? callback; public MessageActionEvoked? callback;
} }

View file

@ -112,6 +112,9 @@ set(MAIN_DEFINITIONS)
if(GTK4_VERSION VERSION_GREATER_EQUAL "4.6") if(GTK4_VERSION VERSION_GREATER_EQUAL "4.6")
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_6) set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_6)
endif() endif()
if(GTK4_VERSION VERSION_GREATER_EQUAL "4.8")
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_8)
endif()
if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.2") if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.2")
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} Adw_1_2) set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} Adw_1_2)
endif() endif()
@ -204,7 +207,9 @@ SOURCES
src/ui/util/label_hybrid.vala src/ui/util/label_hybrid.vala
src/ui/util/sizing_bin.vala src/ui/util/sizing_bin.vala
src/ui/util/size_request_box.vala src/ui/util/size_request_box.vala
src/ui/util/scaling_image.vala
src/ui/widgets/fixed_ratio_picture.vala
src/ui/widgets/natural_size_increase.vala
CUSTOM_VAPIS CUSTOM_VAPIS
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
${CMAKE_BINARY_DIR}/exports/qlite.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi

View file

@ -54,44 +54,60 @@
<object class="GtkBox" id="right_box"> <object class="GtkBox" id="right_box">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkOverlay"> <object class="AdwFlap" id="search_flap">
<property name="child"> <property name="flap-position">end</property>
<object class="GtkStack" id="right_stack"> <property name="modal">true</property>
<property name="locked">true</property>
<property name="swipe-to-open">false</property>
<property name="fold-threshold-policy">natural</property>
<property name="hexpand">true</property>
<child type="content">
<object class="DinoUiNaturalSizeIncrease">
<property name="min-natural-width">600</property>
<child> <child>
<object class="GtkStackPage"> <object class="GtkStack" id="right_stack">
<property name="name">content</property> <property name="hexpand">false</property>
<property name="child"> <child>
<object class="DinoUiConversationView" id="conversation_view"> <object class="GtkStackPage">
<property name="name">content</property>
<property name="child">
<object class="DinoUiConversationView" id="conversation_view">
</object>
</property>
</object> </object>
</property> </child>
</object> <child>
</child> <object class="GtkStackPage">
<child> <property name="name">placeholder</property>
<object class="GtkStackPage"> <property name="child">
<property name="name">placeholder</property> <object class="AdwStatusPage">
<property name="child"> <property name="icon-name">im.dino.Dino-symbolic</property>
<object class="AdwStatusPage"> <property name="hexpand">True</property>
<property name="icon-name">im.dino.Dino-symbolic</property> <property name="vexpand">True</property>
<property name="hexpand">True</property> </object>
<property name="vexpand">True</property> </property>
</object> </object>
</property> </child>
</object> </object>
</child> </child>
</object> </object>
</property> </child>
<child type="overlay"> <child type="separator">
<object class="GtkRevealer" id="search_revealer"> <object class="GtkSeparator" />
<property name="halign">end</property> </child>
<property name="transition-type">slide-left</property> <child type="flap">
<style> <object class="AdwClamp">
<class name="dino-sidebar"/> <property name="hexpand">false</property>
</style> <property name="maximum-size">400</property>
<property name="child"> <property name="tightening-threshold">400</property>
<object class="GtkFrame" id="search_frame"> <child>
<property name="width-request">400</property> <object class="AdwBin" id="search_frame">
<property name="hexpand">true</property>
<style>
<class name="background"/>
</style>
</object> </object>
</property> </child>
</object> </object>
</child> </child>
</object> </object>

View file

@ -37,7 +37,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
private uint time_update_timeout = 0; private uint time_update_timeout = 0;
private ulong updated_roster_handler_id = 0; private ulong updated_roster_handler_id = 0;
public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item, bool initial_item) { public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.conversation = conversation; this.conversation = conversation;
this.item = item; this.item = item;

View file

@ -24,7 +24,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null; private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items); private Gee.TreeSet<ContentMetaItem> content_items = new Gee.TreeSet<ContentMetaItem>(compare_content_meta_items);
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items); private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>(); private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>(); private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
@ -37,7 +37,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
private double? was_page_size; private double? was_page_size;
private Mutex reloading_mutex = Mutex(); private Mutex reloading_mutex = Mutex();
private bool animate = false;
private bool firstLoad = true; private bool firstLoad = true;
private bool at_current_content = true; private bool at_current_content = true;
private bool reload_messages = true; private bool reload_messages = true;
@ -82,6 +81,15 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
main.add_controller(main_motion_events); main.add_controller(main_motion_events);
main_motion_events.motion.connect(update_highlight); main_motion_events.motion.connect(update_highlight);
// Process touch events and capture phase to allow highlighting a message without cursor
GestureClick click_controller = new GestureClick();
click_controller.touch_only = true;
click_controller.propagation_phase = Gtk.PropagationPhase.CAPTURE;
main_wrap_box.add_controller(click_controller);
click_controller.pressed.connect_after((n, x, y) => {
update_highlight(x, y);
});
return this; return this;
} }
@ -200,6 +208,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
MenuButton button = new MenuButton(); MenuButton button = new MenuButton();
button.icon_name = message_actions[i].icon_name; button.icon_name = message_actions[i].icon_name;
button.set_popover(message_actions[i].popover as Popover); button.set_popover(message_actions[i].popover as Popover);
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
action_buttons.add(button); action_buttons.add(button);
} }
@ -210,6 +219,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
button.clicked.connect(() => { button.clicked.connect(() => {
message_action.callback(button, current_meta_item, currently_highlighted); message_action.callback(button, current_meta_item, currently_highlighted);
}); });
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
action_buttons.add(button); action_buttons.add(button);
} }
} }
@ -232,12 +242,71 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
}); });
firstLoad = false; firstLoad = false;
} }
if (conversation == this.conversation && at_current_content) {
// Just make sure we are scrolled down
if (scrolled.vadjustment.value != scrolled.vadjustment.upper) {
scroll_animation(scrolled.vadjustment.upper).play();
}
return;
}
clear(); clear();
initialize_for_conversation_(conversation); initialize_for_conversation_(conversation);
display_latest(); display_latest();
at_current_content = true;
// Scroll to end
scrolled.vadjustment.value = scrolled.vadjustment.upper;
}
private void scroll_and_highlight_item(Plugins.MetaConversationItem target, uint duration = 500) {
Widget widget = null;
int h = 0;
foreach (Plugins.MetaConversationItem item in meta_items) {
widget = widgets[item];
if (target == item) {
break;
}
h += widget.get_allocated_height();
}
if (widget != widgets[target]) {
warning("Target item widget not reached");
return;
}
double target_height = h - scrolled.vadjustment.page_size * 1/3;
Adw.Animation animation = scroll_animation(target_height);
animation.done.connect(() => {
widget.remove_css_class("highlight-once");
widget.add_css_class("highlight-once");
Timeout.add(5000, () => {
widget.remove_css_class("highlight-once");
return false;
});
});
animation.play();
}
private Adw.Animation scroll_animation(double target) {
#if ADW_1_2
return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500,
new Adw.PropertyAnimationTarget(scrolled.vadjustment, "value")
);
#else
return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500,
new Adw.CallbackAnimationTarget(value => {
scrolled.vadjustment.value = value;
})
);
#endif
} }
public void initialize_around_message(Conversation conversation, ContentItem content_item) { public void initialize_around_message(Conversation conversation, ContentItem content_item) {
if (conversation == this.conversation) {
ContentMetaItem? matching_item = content_items.first_match(it => it.content_item.id == content_item.id);
if (matching_item != null) {
scroll_and_highlight_item(matching_item);
return;
}
}
clear(); clear();
initialize_for_conversation_(conversation); initialize_for_conversation_(conversation);
Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40); Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
@ -245,7 +314,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
do_insert_item(item); do_insert_item(item);
} }
ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item);
meta_item.can_merge = false;
Widget w = insert_new(meta_item); Widget w = insert_new(meta_item);
content_items.add(meta_item); content_items.add(meta_item);
meta_items.add(meta_item); meta_items.add(meta_item);
@ -261,23 +329,16 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
// Compute where to jump to for centered message, jump, highlight. // Compute where to jump to for centered message, jump, highlight.
reload_messages = false; reload_messages = false;
Timeout.add(700, () => { Timeout.add(700, () => {
int h = 0, i = 0; scroll_and_highlight_item(meta_item, 300);
foreach (Plugins.MetaConversationItem item in meta_items) {
Widget widget = widgets[item];
if (widget == w) {
break;
}
h += widget.get_allocated_height();
i++;
}
scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
w.add_css_class("highlight-once");
reload_messages = true; reload_messages = true;
return false; return false;
}); });
} }
private void initialize_for_conversation_(Conversation? conversation) { private void initialize_for_conversation_(Conversation? conversation) {
if (this.conversation == conversation) {
print("Re-initialized for %s\n", conversation.counterpart.bare_jid.to_string());
}
// Deinitialize old conversation // Deinitialize old conversation
Dino.Application app = Dino.Application.get_default(); Dino.Application app = Dino.Application.get_default();
if (this.conversation != null) { if (this.conversation != null) {
@ -299,9 +360,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
} }
content_populator.init(this, conversation, Plugins.WidgetType.GTK4); content_populator.init(this, conversation, Plugins.WidgetType.GTK4);
subscription_notification.init(conversation, this); subscription_notification.init(conversation, this);
animate = false;
Timeout.add(20, () => { animate = true; return false; });
} }
private void display_latest() { private void display_latest() {
@ -331,8 +389,8 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
public void do_insert_item(Plugins.MetaConversationItem item) { public void do_insert_item(Plugins.MetaConversationItem item) {
lock (meta_items) { lock (meta_items) {
insert_new(item); insert_new(item);
if (item as ContentMetaItem != null) { if (item is ContentMetaItem) {
content_items.add(item); content_items.add((ContentMetaItem)item);
} }
meta_items.add(item); meta_items.add(item);
} }
@ -348,7 +406,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
widget_order.remove(skeleton.get_widget()); widget_order.remove(skeleton.get_widget());
item_item_skeletons.unset(item); item_item_skeletons.unset(item);
content_items.remove(item); if (item is ContentMetaItem) {
content_items.remove((ContentMetaItem)item);
}
meta_items.remove(item); meta_items.remove(item);
} }
@ -387,7 +447,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
Plugins.MetaConversationItem? lower_item = meta_items.lower(item); Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
// Fill datastructure // Fill datastructure
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate); ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item);
item_item_skeletons[item] = item_skeleton; item_item_skeletons[item] = item_skeleton;
int index = lower_item != null ? widget_order.index_of(item_item_skeletons[lower_item].get_widget()) + 1 : 0; int index = lower_item != null ? widget_order.index_of(item_item_skeletons[lower_item].get_widget()) + 1 : 0;
widget_order.insert(index, item_skeleton.get_widget()); widget_order.insert(index, item_skeleton.get_widget());
@ -503,6 +563,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
} }
} }
private static int compare_content_meta_items(ContentMetaItem a, ContentMetaItem b) {
return compare_meta_items(a, b);
}
private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
int cmp1 = a.time.compare(b.time); int cmp1 = a.time.compare(b.time);
if (cmp1 != 0) return cmp1; if (cmp1 != 0) return cmp1;

View file

@ -10,9 +10,6 @@ namespace Dino.Ui {
public class FileDefaultWidget : Box { public class FileDefaultWidget : Box {
public signal void clicked(); public signal void clicked();
public signal void open_file();
public signal void save_file_as();
public signal void cancel_download();
[GtkChild] public unowned Stack image_stack; [GtkChild] public unowned Stack image_stack;
[GtkChild] public unowned Label name_label; [GtkChild] public unowned Label name_label;
@ -23,12 +20,6 @@ public class FileDefaultWidget : Box {
private FileTransfer.State state; private FileTransfer.State state;
class construct {
install_action("file.open", null, (widget, action_name) => { ((FileDefaultWidget) widget).open_file(); });
install_action("file.save_as", null, (widget, action_name) => { ((FileDefaultWidget) widget).save_file_as(); });
install_action("file.cancel", null, (widget, action_name) => { ((FileDefaultWidget) widget).cancel_download(); });
}
public FileDefaultWidget() { public FileDefaultWidget() {
EventControllerMotion this_motion_events = new EventControllerMotion(); EventControllerMotion this_motion_events = new EventControllerMotion();
this.add_controller(this_motion_events); this.add_controller(this_motion_events);

View file

@ -8,41 +8,70 @@ namespace Dino.Ui {
public class FileImageWidget : Box { public class FileImageWidget : Box {
FileDefaultWidget file_default_widget;
FileDefaultWidgetController file_default_widget_controller;
public FileImageWidget() { public FileImageWidget() {
this.halign = Align.START; this.halign = Align.START;
this.add_css_class("file-image-widget"); this.add_css_class("file-image-widget");
this.set_cursor_from_name("zoom-in");
} }
public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error { public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error {
FixedRatioPicture image = new FixedRatioPicture() { min_width = 100, min_height = 100, max_width = MAX_WIDTH, max_height = MAX_HEIGHT, file = file }; Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.HORIZONTAL, 0) { halign=Gtk.Align.END, valign=Gtk.Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
image_overlay_toolbar.add_css_class("card");
image_overlay_toolbar.add_css_class("toolbar");
image_overlay_toolbar.add_css_class("overlay-toolbar");
image_overlay_toolbar.set_cursor_from_name("default");
FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=MAX_WIDTH, max_height=MAX_HEIGHT, file=file };
GestureClick gesture_click_controller = new GestureClick();
gesture_click_controller.button = 1; // listen for left clicks
gesture_click_controller.released.connect((n_press, x, y) => {
switch (gesture_click_controller.get_device().source) {
case Gdk.InputSource.TOUCHSCREEN:
case Gdk.InputSource.PEN:
if (n_press == 1) {
image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
} else if (n_press == 2) {
this.activate_action("file.open", null);
image_overlay_toolbar.visible = false;
}
break;
default:
this.activate_action("file.open", null);
image_overlay_toolbar.visible = false;
break;
}
});
image.add_controller(gesture_click_controller);
FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE);
string? mime_type = file_info.get_content_type(); string? mime_type = file_info.get_content_type();
file_default_widget = new FileDefaultWidget() { valign=Align.END, vexpand=false, visible=false }; MenuButton button = new MenuButton();
file_default_widget.image_stack.visible = false; button.icon_name = "open-menu";
file_default_widget_controller = new FileDefaultWidgetController(file_default_widget); Menu menu_model = new Menu();
file_default_widget_controller.set_file(file, file_name, mime_type); menu_model.append(_("Open"), "file.open");
menu_model.append(_("Save as…"), "file.save_as");
Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model);
button.popover = popover_menu;
image_overlay_toolbar.append(button);
Overlay overlay = new Overlay(); Overlay overlay = new Overlay();
overlay.set_child(image); overlay.set_child(image);
overlay.add_overlay(file_default_widget); overlay.add_overlay(image_overlay_toolbar);
overlay.set_measure_overlay(image, true); overlay.set_measure_overlay(image, true);
overlay.set_clip_overlay(file_default_widget, true); overlay.set_clip_overlay(image_overlay_toolbar, true);
EventControllerMotion this_motion_events = new EventControllerMotion(); EventControllerMotion this_motion_events = new EventControllerMotion();
this.add_controller(this_motion_events); this.add_controller(this_motion_events);
this_motion_events.enter.connect(() => { this_motion_events.enter.connect(() => {
file_default_widget.visible = true; image_overlay_toolbar.visible = true;
}); });
this_motion_events.leave.connect(() => { this_motion_events.leave.connect(() => {
if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return; if (button.popover != null && button.popover.visible) return;
file_default_widget.visible = false; image_overlay_toolbar.visible = false;
}); });
this.append(overlay); this.append(overlay);

View file

@ -21,7 +21,9 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
} }
public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) {
return new FileWidget(stream_interactor, file_transfer); FileWidget widget = new FileWidget(file_transfer);
FileWidgetController widget_controller = new FileWidgetController(widget, file_transfer, stream_interactor);
return widget;
} }
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
@ -57,7 +59,6 @@ public class FileWidget : SizeRequestBox {
DEFAULT DEFAULT
} }
private StreamInteractor stream_interactor;
private FileTransfer file_transfer; private FileTransfer file_transfer;
public FileTransfer.State file_transfer_state { get; set; } public FileTransfer.State file_transfer_state { get; set; }
public string file_transfer_mime_type { get; set; } public string file_transfer_mime_type { get; set; }
@ -66,13 +67,24 @@ public class FileWidget : SizeRequestBox {
private FileDefaultWidgetController default_widget_controller; private FileDefaultWidgetController default_widget_controller;
private Widget? content = null; private Widget? content = null;
public signal void open_file();
public signal void save_file_as();
public signal void start_download();
public signal void cancel_download();
class construct {
install_action("file.open", null, (widget, action_name) => { ((FileWidget) widget).open_file(); });
install_action("file.save_as", null, (widget, action_name) => { ((FileWidget) widget).save_file_as(); });
install_action("file.download", null, (widget, action_name) => { ((FileWidget) widget).start_download(); });
install_action("file.cancel", null, (widget, action_name) => { ((FileWidget) widget).cancel_download(); });
}
construct { construct {
margin_top = 4; margin_top = 4;
size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
} }
public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) { public FileWidget(FileTransfer file_transfer) {
this.stream_interactor = stream_interactor;
this.file_transfer = file_transfer; this.file_transfer = file_transfer;
update_widget.begin(); update_widget.begin();
@ -113,7 +125,7 @@ public class FileWidget : SizeRequestBox {
if (content != null) this.remove(content); if (content != null) this.remove(content);
FileDefaultWidget default_file_widget = new FileDefaultWidget(); FileDefaultWidget default_file_widget = new FileDefaultWidget();
default_widget_controller = new FileDefaultWidgetController(default_file_widget); default_widget_controller = new FileDefaultWidgetController(default_file_widget);
default_widget_controller.set_file_transfer(file_transfer, stream_interactor); default_widget_controller.set_file_transfer(file_transfer);
content = default_file_widget; content = default_file_widget;
this.state = State.DEFAULT; this.state = State.DEFAULT;
this.append(content); this.append(content);
@ -138,94 +150,104 @@ public class FileWidget : SizeRequestBox {
} }
} }
public class FileDefaultWidgetController : Object { public class FileWidgetController : Object {
private FileDefaultWidget widget;
private FileTransfer? file_transfer;
public string file_transfer_path { get; set; }
public string file_transfer_state { get; set; }
public string file_transfer_mime_type { get; set; }
private weak Widget widget;
private FileTransfer file_transfer;
private StreamInteractor? stream_interactor; private StreamInteractor? stream_interactor;
private string file_uri;
private string file_name;
private FileTransfer.State state;
public FileDefaultWidgetController(FileDefaultWidget widget) { public FileWidgetController(FileWidget widget, FileTransfer file_transfer, StreamInteractor? stream_interactor = null) {
this.widget = widget; this.widget = widget;
this.ref();
widget.clicked.connect(on_clicked); this.widget.weak_ref(() => {
widget.open_file.connect(open_file); this.widget = null;
widget.save_file_as.connect(save_file); this.unref();
widget.cancel_download.connect(cancel_download); });
}
public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) {
this.file_transfer = file_transfer; this.file_transfer = file_transfer;
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
widget.name_label.label = file_name = file_transfer.file_name; widget.open_file.connect(open_file);
widget.save_file_as.connect(save_file);
file_transfer.bind_property("path", this, "file-transfer-path"); widget.start_download.connect(start_download);
file_transfer.bind_property("state", this, "file-transfer-state"); widget.cancel_download.connect(cancel_download);
file_transfer.bind_property("mime-type", this, "file-transfer-mime-type");
this.notify["file-transfer-path"].connect(update_file_info);
this.notify["file-transfer-state"].connect(update_file_info);
this.notify["file-transfer-mime-type"].connect(update_file_info);
update_file_info();
}
public void set_file(File file, string file_name, string? mime_type) {
file_uri = file.get_uri();
state = FileTransfer.State.COMPLETE;
widget.name_label.label = this.file_name = file_name;
widget.update_file_info(mime_type, state, -1);
}
private void update_file_info() {
file_uri = file_transfer.get_file().get_uri();
state = file_transfer.state;
widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size);
} }
private void open_file() { private void open_file() {
try{ try{
AppInfo.launch_default_for_uri(file_uri, null); AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
} catch (Error err) { } catch (Error err) {
warning("Failed to open %s - %s", file_uri, err.message); warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message);
} }
} }
private void save_file() { private void save_file() {
var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null); var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null);
save_dialog.set_modal(true); save_dialog.set_modal(true);
save_dialog.set_current_name(file_name); save_dialog.set_current_name(file_transfer.file_name);
save_dialog.response.connect(() => { save_dialog.response.connect(() => {
try{ try{
GLib.File.new_for_uri(file_uri).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); GLib.File.new_for_uri(file_transfer.get_file().get_uri()).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null);
} catch (Error err) { } catch (Error err) {
warning("Failed copy file %s - %s", file_uri, err.message); warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message);
} }
}); });
save_dialog.show(); save_dialog.show();
} }
private void start_download() {
if (stream_interactor != null) {
stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer);
}
}
private void cancel_download() { private void cancel_download() {
file_transfer.cancellable.cancel(); file_transfer.cancellable.cancel();
} }
}
public class FileDefaultWidgetController : Object {
private FileDefaultWidget widget;
private FileTransfer? file_transfer;
public string file_transfer_state { get; set; }
public string file_transfer_mime_type { get; set; }
private FileTransfer.State state;
public FileDefaultWidgetController(FileDefaultWidget widget) {
this.widget = widget;
widget.clicked.connect(on_clicked);
this.notify["file-transfer-state"].connect(update_file_info);
this.notify["file-transfer-mime-type"].connect(update_file_info);
}
public void set_file_transfer(FileTransfer file_transfer) {
this.file_transfer = file_transfer;
widget.name_label.label = file_transfer.file_name;
file_transfer.bind_property("state", this, "file-transfer-state");
file_transfer.bind_property("mime-type", this, "file-transfer-mime-type");
update_file_info();
}
private void update_file_info() {
state = file_transfer.state;
widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size);
}
private void on_clicked() { private void on_clicked() {
switch (state) { switch (state) {
case FileTransfer.State.COMPLETE: case FileTransfer.State.COMPLETE:
open_file(); widget.activate_action("file.open", null);
break; break;
case FileTransfer.State.NOT_STARTED: case FileTransfer.State.NOT_STARTED:
assert(stream_interactor != null && file_transfer != null); widget.activate_action("file.download", null);
stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer);
break; break;
default: default:
// Clicking doesn't do anything in FAILED and IN_PROGRESS states // Clicking doesn't do anything in FAILED and IN_PROGRESS states

View file

@ -217,6 +217,7 @@ public class MessageMetaItem : ContentMetaItem {
if (correction_allowed) { if (correction_allowed) {
Plugins.MessageAction action1 = new Plugins.MessageAction(); Plugins.MessageAction action1 = new Plugins.MessageAction();
action1.icon_name = "document-edit-symbolic"; action1.icon_name = "document-edit-symbolic";
action1.tooltip = _("Edit message");
action1.callback = (button, content_meta_item_activated, widget) => { action1.callback = (button, content_meta_item_activated, widget) => {
this.in_edit_mode = true; this.in_edit_mode = true;
}; };
@ -225,6 +226,7 @@ public class MessageMetaItem : ContentMetaItem {
Plugins.MessageAction reply_action = new Plugins.MessageAction(); Plugins.MessageAction reply_action = new Plugins.MessageAction();
reply_action.icon_name = "mail-reply-sender-symbolic"; reply_action.icon_name = "mail-reply-sender-symbolic";
reply_action.tooltip = _("Reply");
reply_action.callback = (button, content_meta_item_activated, widget) => { reply_action.callback = (button, content_meta_item_activated, widget) => {
GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) })); GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) }));
}; };
@ -233,6 +235,7 @@ public class MessageMetaItem : ContentMetaItem {
if (supports_reaction) { if (supports_reaction) {
Plugins.MessageAction action2 = new Plugins.MessageAction(); Plugins.MessageAction action2 = new Plugins.MessageAction();
action2.icon_name = "dino-emoticon-add-symbolic"; action2.icon_name = "dino-emoticon-add-symbolic";
action2.tooltip = _("Add reaction");
EmojiChooser chooser = new EmojiChooser(); EmojiChooser chooser = new EmojiChooser();
chooser.emoji_picked.connect((emoji) => { chooser.emoji_picked.connect((emoji) => {
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji); stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji);

View file

@ -151,6 +151,8 @@ public class GlobalSearch {
} }
private void clear_search() { private void clear_search() {
// Scroll to top
results_scrolled.vadjustment.value = 0;
foreach (Widget widget in results_box_children) { foreach (Widget widget in results_box_children) {
results_box.remove(widget); results_box.remove(widget);
} }

View file

@ -23,7 +23,7 @@ public class MainWindow : Adw.Window {
public Adw.Leaflet leaflet; public Adw.Leaflet leaflet;
public Box left_box; public Box left_box;
public Box right_box; public Box right_box;
public Revealer search_revealer; public Adw.Flap search_flap;
public GlobalSearch global_search; public GlobalSearch global_search;
private Stack stack = new Stack(); private Stack stack = new Stack();
private Stack left_stack; private Stack left_stack;
@ -35,7 +35,7 @@ public class MainWindow : Adw.Window {
class construct { class construct {
var shortcut = new Shortcut(new KeyvalTrigger(Key.F, ModifierType.CONTROL_MASK), new CallbackAction((widget, args) => { var shortcut = new Shortcut(new KeyvalTrigger(Key.F, ModifierType.CONTROL_MASK), new CallbackAction((widget, args) => {
((MainWindow) widget).search_revealer.reveal_child = true; ((MainWindow) widget).search_flap.reveal_flap = true;
return false; return false;
})); }));
add_shortcut(shortcut); add_shortcut(shortcut);
@ -67,11 +67,11 @@ public class MainWindow : Adw.Window {
left_stack = (Stack) builder.get_object("left_stack"); left_stack = (Stack) builder.get_object("left_stack");
right_stack = (Stack) builder.get_object("right_stack"); right_stack = (Stack) builder.get_object("right_stack");
conversation_view = (ConversationView) builder.get_object("conversation_view"); conversation_view = (ConversationView) builder.get_object("conversation_view");
search_revealer = (Revealer) builder.get_object("search_revealer"); search_flap = (Adw.Flap) builder.get_object("search_flap");
conversation_selector = ((ConversationSelector) builder.get_object("conversation_list")).init(stream_interactor); conversation_selector = ((ConversationSelector) builder.get_object("conversation_list")).init(stream_interactor);
conversation_selector.conversation_selected.connect_after(() => leaflet.navigate(Adw.NavigationDirection.FORWARD)); conversation_selector.conversation_selected.connect_after(() => leaflet.navigate(Adw.NavigationDirection.FORWARD));
Frame search_frame = (Frame) builder.get_object("search_frame"); Adw.Bin search_frame = (Adw.Bin) builder.get_object("search_frame");
global_search = new GlobalSearch(stream_interactor); global_search = new GlobalSearch(stream_interactor);
search_frame.set_child(global_search.get_widget()); search_frame.set_child(global_search.get_widget());
} }

View file

@ -45,10 +45,10 @@ public class MainWindowController : Object {
this.conversation_view_controller = new ConversationViewController(window.conversation_view, window.conversation_titlebar, stream_interactor); this.conversation_view_controller = new ConversationViewController(window.conversation_view, window.conversation_titlebar, stream_interactor);
conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_revealer, "reveal_child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_flap, "reveal-flap", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
window.search_revealer.notify["child-revealed"].connect(() => { window.search_flap.notify["reveal-flap"].connect(() => {
if (window.search_revealer.child_revealed) { if (window.search_flap.reveal_flap) {
if (window.conversation_view.conversation_frame.conversation != null && window.global_search.search_entry.text == "") { if (window.conversation_view.conversation_frame.conversation != null && window.global_search.search_entry.text == "") {
reset_search_entry(); reset_search_entry();
} }
@ -59,7 +59,9 @@ public class MainWindowController : Object {
window.global_search.selected_item.connect((item) => { window.global_search.selected_item.connect((item) => {
select_conversation(item.conversation, false, false); select_conversation(item.conversation, false, false);
window.conversation_view.conversation_frame.initialize_around_message(item.conversation, item); window.conversation_view.conversation_frame.initialize_around_message(item.conversation, item);
close_search(); if (window.search_flap.folded) {
close_search();
}
}); });
window.welcome_placeholder.primary_button.clicked.connect(() => { window.welcome_placeholder.primary_button.clicked.connect(() => {
@ -91,16 +93,6 @@ public class MainWindowController : Object {
Widget window_widget = ((Widget) window); Widget window_widget = ((Widget) window);
GestureClick gesture_click_controller = new GestureClick();
window_widget.add_controller(gesture_click_controller);
gesture_click_controller.pressed.connect((n_press, click_x, click_y) => {
double search_x, search_y;
bool ret = window.search_revealer.translate_coordinates(window, 0, 0, out search_x, out search_y);
if (ret && click_x < search_x) {
close_search();
}
});
EventControllerKey key_event_controller = new EventControllerKey(); EventControllerKey key_event_controller = new EventControllerKey();
window_widget.add_controller(key_event_controller); window_widget.add_controller(key_event_controller);
// TODO GTK4: Why doesn't this work with key_pressed signal // TODO GTK4: Why doesn't this work with key_pressed signal

View file

@ -1,131 +0,0 @@
using Gdk;
using Gtk;
namespace Dino.Ui {
class FixedRatioLayout : Gtk.LayoutManager {
public int min_width { get; set; default = 0; }
public int target_width { get; set; default = -1; }
public int max_width { get; set; default = int.MAX; }
public int min_height { get; set; default = 0; }
public int target_height { get; set; default = -1; }
public int max_height { get; set; default = int.MAX; }
public FixedRatioLayout() {
this.notify.connect(layout_changed);
}
private void measure_target_size(Gtk.Widget widget, out int width, out int height) {
if (target_width != -1 && target_height != -1) {
width = target_width;
height = target_height;
return;
}
Widget child;
width = min_width;
height = min_height;
child = widget.get_first_child();
while (child != null) {
if (child.should_layout()) {
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
width = int.max(child_nat, width);
}
child = child.get_next_sibling();
}
width = int.min(width, max_width);
child = widget.get_first_child();
while (child != null) {
if (child.should_layout()) {
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
height = int.max(child_nat, height);
}
child = child.get_next_sibling();
}
if (height > max_height) {
height = max_height;
width = min_width;
child = widget.get_first_child();
while (child != null) {
if (child.should_layout()) {
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
width = int.max(child_nat, width);
}
child = child.get_next_sibling();
}
width = int.min(width, max_width);
}
}
public override void measure(Gtk.Widget widget, Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
minimum_baseline = -1;
natural_baseline = -1;
int width, height;
measure_target_size(widget, out width, out height);
if (orientation == Orientation.HORIZONTAL) {
minimum = min_width;
natural = width;
} else if (for_size == -1) {
minimum = min_height;
natural = height;
} else {
minimum = natural = height * for_size / width;
}
}
public override void allocate(Gtk.Widget widget, int width, int height, int baseline) {
Widget child = widget.get_first_child();
while (child != null) {
if (child.should_layout()) {
child.allocate(width, height, baseline, null);
}
child = child.get_next_sibling();
}
}
public override SizeRequestMode get_request_mode(Gtk.Widget widget) {
return SizeRequestMode.HEIGHT_FOR_WIDTH;
}
}
class FixedRatioPicture : Gtk.Widget {
public int min_width { get { return layout.min_width; } set { layout.min_width = value; } }
public int target_width { get { return layout.target_width; } set { layout.target_width = value; } }
public int max_width { get { return layout.max_width; } set { layout.max_width = value; } }
public int min_height { get { return layout.min_height; } set { layout.min_height = value; } }
public int target_height { get { return layout.target_height; } set { layout.target_height = value; } }
public int max_height { get { return layout.max_height; } set { layout.max_height = value; } }
public File file { get { return inner.file; } set { inner.file = value; } }
public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } }
#if GTK_4_8 && VALA_0_58
public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } }
#endif
private Gtk.Picture inner = new Gtk.Picture();
private FixedRatioLayout layout = new FixedRatioLayout();
public FixedRatioPicture() {
layout_manager = layout;
inner.insert_after(this, null);
}
public override void dispose() {
inner.unparent();
base.dispose();
}
}
}

View file

@ -0,0 +1,88 @@
using Gdk;
using Gtk;
class Dino.Ui.FixedRatioPicture : Gtk.Widget {
public int min_width { get; set; default = -1; }
public int max_width { get; set; default = int.MAX; }
public int min_height { get; set; default = -1; }
public int max_height { get; set; default = int.MAX; }
public File file { get { return inner.file; } set { inner.file = value; } }
public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } }
#if GTK_4_8 && VALA_0_58
public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } }
#endif
private Gtk.Picture inner = new Gtk.Picture();
construct {
set_css_name("picture");
add_css_class("fixed-ratio");
inner.insert_after(this, null);
this.notify.connect(queue_resize);
}
private void measure_target_size(out int width, out int height) {
if (width_request != -1 && height_request != -1) {
width = width_request;
height = height_request;
return;
}
width = min_width;
height = min_height;
if (inner.should_layout()) {
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
inner.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
width = int.max(child_nat, width);
}
width = int.min(width, max_width);
if (inner.should_layout()) {
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
inner.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
height = int.max(child_nat, height);
}
if (height > max_height) {
height = max_height;
width = min_width;
if (inner.should_layout()) {
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
inner.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
width = int.max(child_nat, width);
}
width = int.min(width, max_width);
}
}
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
minimum_baseline = -1;
natural_baseline = -1;
int width, height;
measure_target_size(out width, out height);
if (orientation == Orientation.HORIZONTAL) {
minimum = min_width;
natural = width;
} else if (for_size == -1) {
minimum = min_height;
natural = height;
} else {
minimum = natural = height * for_size / width;
}
}
public override void size_allocate(int width, int height, int baseline) {
if (inner.should_layout()) {
inner.allocate(width, height, baseline, null);
}
}
public override SizeRequestMode get_request_mode() {
return SizeRequestMode.HEIGHT_FOR_WIDTH;
}
public override void dispose() {
inner.unparent();
base.dispose();
}
}

View file

@ -0,0 +1,59 @@
using Gtk;
public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget {
public int min_natural_height { get; set; default = -1; }
public int min_natural_width { get; set; default = -1; }
construct {
this.notify.connect(queue_resize);
}
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
minimum = 0;
if (orientation == Orientation.HORIZONTAL) {
natural = min_natural_width;
} else {
natural = min_natural_height;
}
natural = int.max(0, natural);
minimum_baseline = -1;
natural_baseline = -1;
Widget child = get_first_child();
while (child != null) {
if (child.should_layout()) {
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(orientation, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
minimum = int.max(minimum, child_min);
natural = int.max(natural, child_nat);
if (child_min_baseline > 0) {
minimum_baseline = int.max(minimum_baseline, child_min_baseline);
}
if (child_nat_baseline > 0) {
natural_baseline = int.max(natural_baseline, child_nat_baseline);
}
}
child = child.get_next_sibling();
}
}
public override void size_allocate(int width, int height, int baseline) {
Widget child = get_first_child();
while (child != null) {
if (child.should_layout()) {
child.allocate(width, height, baseline, null);
}
child = child.get_next_sibling();
}
}
public override SizeRequestMode get_request_mode() {
Widget child = get_first_child();
if (child != null) {
return child.get_request_mode();
}
return SizeRequestMode.CONSTANT_SIZE;
}
}