Refactor attachment grid

* Introduce Slotted attachment grid

* Use Stack in attachment Slot

* Display attachment type

* Fit pictures into the Slot center
This commit is contained in:
Bleak Grey 2020-07-10 17:30:57 +03:00 committed by GitHub
parent 3d0bd9e48e
commit a542be90c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 398 additions and 147 deletions

View File

@ -3,9 +3,24 @@
}
.attachment {
border-radius: 4px;
border-radius: 6px;
background: rgba (150, 150, 150, 0.2);
padding:0px;
margin:0px;
}
.attachment .pic {
border-radius: 6px;
}
.attachment .chip {
padding: 6px;
border-radius:6px;
}
/*.attachment box button:nth-child(1) {*/
/* border-radius:0 0 0 8px;*/
/*}*/
/*.attachment box button:nth-child(2) {*/
/* border-radius:0 8px 0 0;*/
/*}*/
.header-title-button {
margin: 0px;

View File

@ -10,6 +10,7 @@
<file preprocess="xml-stripblanks">ui/widgets/accounts_button_item.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/profile_field_row.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/timeline_filter.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/attachment_slot.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/compose.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/main.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/preferences.ui</file>

View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<template class="TootleWidgetsAttachmentSlot" parent="GtkFlowBoxChild">
<property name="height_request">180</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkGrid" id="overlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkImage" id="play_icon">
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_start">16</property>
<property name="margin_end">16</property>
<property name="margin_top">16</property>
<property name="margin_bottom">16</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="pixel_size">48</property>
<property name="icon_name">media-playback-start-symbolic</property>
<property name="icon_size">0</property>
<style>
<class name="chip"/>
<class name="osd"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="edit_bar">
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="margin_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkButton" id="edit_btn">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Describe for the visually impaired</property>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-edit-symbolic</property>
</object>
</child>
<style>
<class name="osd"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_btn">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
<style>
<class name="osd"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="chip">
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="margin_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<style>
<class name="osd"/>
<class name="chip"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="interpolate_size">True</property>
<child>
<object class="GtkSpinner" id="loading">
<property name="width_request">32</property>
<property name="height_request">32</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="attachment"/>
</style>
</template>
</interface>

View File

@ -79,7 +79,8 @@ executable(
'src/Widgets/Notification.vala',
'src/Widgets/VisibilityPopover.vala',
'src/Widgets/Attachment/Box.vala',
'src/Widgets/Attachment/Item.vala',
'src/Widgets/Attachment/Slot.vala',
'src/Widgets/Attachment/Picture.vala',
'src/Dialogs/ISavedWindow.vala',
'src/Dialogs/MainWindow.vala',
'src/Dialogs/Compose.vala',

View File

@ -5,7 +5,7 @@ public class Tootle.API.Attachment : Entity {
public string url { get; set; }
public string? description { get; set; }
public string? _preview_url { get; set; }
public string preview_url {
public string? preview_url {
set { this._preview_url = value; }
get { return (this._preview_url == null || this._preview_url == "") ? url : _preview_url; }
}

View File

@ -76,6 +76,11 @@ public class Tootle.Desktop {
return theme.has_icon (fallback) ? fallback : fallback2;
}
public static Gdk.Pixbuf icon_to_pixbuf (string name) {
var theme = Gtk.IconTheme.get_default ();
return theme.load_icon (name, 32, Gtk.IconLookupFlags.GENERIC_FALLBACK);
}
public static void set_hotkey_tooltip (Gtk.Widget widget, string? description, string[] accelerators) {
widget.tooltip_markup = Granite.markup_accel_tooltip (accelerators, description);
}

View File

@ -8,7 +8,7 @@ public class Tootle.Dialogs.Compose : Window {
public string label { get; construct set; }
public int char_limit {
get {
return 250;
return 500;
}
}
@ -62,6 +62,7 @@ public class Tootle.Dialogs.Compose : Window {
}
content.buffer.text = Html.remove_tags (status.content);
validate ();
show ();
}

View File

@ -43,10 +43,12 @@ public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow {
timeline_switcher.valign = Align.FILL;
timeline_stack.notify["visible-child"].connect (on_timeline_changed);
add_timeline_view (new Views.Home (), app.ACCEL_TIMELINE_0, 0);
add_timeline_view (new Views.Notifications (), app.ACCEL_TIMELINE_1, 1);
add_timeline_view (new Views.Local (), app.ACCEL_TIMELINE_2, 2);
add_timeline_view (new Views.Federated (), app.ACCEL_TIMELINE_3, 3);
add_timeline_view (new Views.Bookmarks (), app.ACCEL_TIMELINE_0, 0);
add_timeline_view (new Views.Home (), app.ACCEL_TIMELINE_1, 1);
// add_timeline_view (new Views.Home (), app.ACCEL_TIMELINE_0, 0);
// add_timeline_view (new Views.Notifications (), app.ACCEL_TIMELINE_1, 1);
// add_timeline_view (new Views.Local (), app.ACCEL_TIMELINE_2, 2);
// add_timeline_view (new Views.Federated (), app.ACCEL_TIMELINE_3, 3);
settings.bind_property ("dark-theme", Gtk.Settings.get_default (), "gtk-application-prefer-dark-theme", BindingFlags.SYNC_CREATE);
settings.notify["post-text-size"].connect (() => on_zoom_level_changed ());

View File

@ -13,24 +13,4 @@ public class Tootle.Drawing {
ctx.close_path ();
}
public static void center (Cairo.Context ctx, int w, int h, int tw, int th) {
var cx = w/2 - tw/2;
var cy = h/2 - th/2;
ctx.translate (cx, cy);
}
public static Pixbuf make_thumbnail (Pixbuf pb, int view_w, int view_h) {
if (view_w >= pb.width && view_h >= pb.height)
return pb;
double ratio_x = (double) view_w / (double) pb.width;
double ratio_y = (double) view_h / (double) pb.height;
double ratio = ratio_x < ratio_y ? ratio_x : ratio_y;
return pb.scale_simple (
(int) (pb.width * ratio),
(int) (pb.height * ratio),
InterpType.BILINEAR);
}
}

View File

@ -81,10 +81,22 @@ public class Tootle.Cache : GLib.Object {
id = msg.finished.connect (() => {
Pixbuf? pixbuf = null;
var data = msg.response_body.flatten ().data;
var stream = new MemoryInputStream.from_data (data);
pixbuf = new Pixbuf.from_stream (stream);
stream.close ();
try {
var code = message.status_code;
if (code != Soup.Status.OK) {
var msg = network.describe_error (code);
throw new Oopsie.INSTANCE (@"Server returned $msg");
}
var data = message.response_body.flatten ().data;
var stream = new MemoryInputStream.from_data (data);
pixbuf = new Pixbuf.from_stream (stream);
stream.close ();
}
catch (Error e) {
warning (@"\"$url\" -> Pixbuf: FAIL ($(e.message))");
pixbuf = Desktop.icon_to_pixbuf ("image-x-generic-symbolic");
}
// message (@"[*] $key");
items[key] = new Item (pixbuf, 1);

View File

@ -68,7 +68,7 @@ public class Tootle.Network : GLib.Object {
});
}
public string describe_error (int32 code) {
public string describe_error (uint code) {
var reason = Soup.Status.get_phrase (code);
return @"$code: $reason";
}

View File

@ -9,6 +9,7 @@ public class Tootle.Widgets.Attachment.Box : FlowBox {
construct {
hexpand = true;
can_focus = false;
column_spacing = row_spacing = 8;
selection_mode = SelectionMode.NONE;
}
@ -16,6 +17,7 @@ public class Tootle.Widgets.Attachment.Box : FlowBox {
Object (editing: editing);
}
//TODO: Upload attachments in Compose dialog
public void select () {
var filter = new Gtk.FileFilter ();
filter.add_mime_type ("image/jpeg");
@ -49,20 +51,27 @@ public class Tootle.Widgets.Attachment.Box : FlowBox {
public bool populate (ArrayList<API.Attachment>? list) {
if (list == null)
return false;
var max = 6;
if (list.size % 2 == 0)
max = 2;
//max_children_per_line = (int)Math.fmin (list.size, 5);
var max = 2;
var min = 1;
if (list.size == 1)
max = 1;
else if (list.size % 2 == 0)
max = min = 2;
else if (list.size % 3 == 0)
max = min = 3;
max_children_per_line = max;
min_children_per_line = min;
list.@foreach (obj => pack (obj));
return true;
}
public bool pack (API.Attachment obj) {
var w = new Widgets.Attachment.Item (obj);
var w = new Widgets.Attachment.Slot (obj);
insert (w, -1);
return true;
}

View File

@ -1,105 +0,0 @@
using Gtk;
using Gdk;
public class Tootle.Widgets.Attachment.Item : EventBox {
public API.Attachment attachment { get; construct set; }
private Cache.Reference? cached;
public Item (API.Attachment obj) {
Object (attachment: obj);
}
~Item () {
cache.unload (cached);
}
construct {
get_style_context ().add_class ("attachment");
width_request = height_request = 128;
hexpand = true;
tooltip_text = attachment.description ?? _("No description is available");
button_press_event.connect (on_clicked);
show ();
on_request ();
}
protected void on_request () {
cached = null;
on_redraw ();
cache.load (attachment.preview_url, on_cache_result);
}
protected void on_redraw () {
var w = get_allocated_width ();
var h = get_allocated_height ();
queue_draw_area (0, 0, w, h);
}
protected void on_cache_result (Cache.Reference? result) {
cached = result;
on_redraw ();
}
protected void download () {
Desktop.download (attachment.url, path => {
app.toast (_("Attachment downloaded"));
});
}
protected void open () {
Desktop.download (attachment.url, path => {
Desktop.open_uri (path);
});
}
public override bool draw (Cairo.Context ctx) {
base.draw (ctx);
var w = get_allocated_width ();
var h = get_allocated_height ();
var style = get_style_context ();
var border_radius = style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, style.get_state ()).get_int ();
if (cached != null) {
if (cached.loading) {
Drawing.center (ctx, w, h, 32, 32);
get_style_context ().render_activity (ctx, 0, 0, 32, 32);
}
else {
var thumb = Drawing.make_thumbnail (cached.data, w, h);
Drawing.draw_rounded_rect (ctx, 0, 0, w, h, border_radius);
Drawing.center (ctx, w, h, thumb.width, thumb.height);
Gdk.cairo_set_source_pixbuf (ctx, thumb, 0, 0);
ctx.fill ();
}
}
return Gdk.EVENT_STOP;
}
protected virtual bool on_clicked (EventButton ev) {
if (ev.button == 1) {
open ();
return true;
}
else if (ev.button == 3) {
var menu = new Gtk.Menu ();
var item_open = new Gtk.MenuItem.with_label (_("Open"));
item_open.activate.connect (open);
menu.add (item_open);
var item_download = new Gtk.MenuItem.with_label (_("Download"));
item_download.activate.connect (download);
menu.add (item_download);
menu.show_all ();
menu.attach_widget = this;
menu.popup_at_pointer ();
return true;
}
return false;
}
}

View File

@ -0,0 +1,91 @@
using Gtk;
using Gdk;
public class Tootle.Widgets.Attachment.Picture : DrawingArea {
public string url { get; set; }
Cache.Reference? cached;
construct {
hexpand = vexpand = true;
get_style_context ().add_class ("pic");
}
public class Picture (string url) {
Object (url: url);
}
~Picture () {
cache.unload (cached);
}
public void on_request () {
cached = null;
on_redraw ();
cache.load (url, on_cache_update);
}
void on_cache_update (Cache.Reference? result) {
cached = result;
if (cached != null)
visible = !cached.loading;
on_redraw ();
}
void on_redraw () {
var w = get_allocated_width ();
var h = get_allocated_height ();
queue_draw_area (0, 0, w, h);
}
float get_ratio (int w, int h) {
var ow = cached.data.get_width ();
var oh = cached.data.get_height ();
var xscale = (float) w / ow;
var yscale = (float) h / oh;
if (xscale > yscale)
return xscale;
else
return yscale;
}
public override bool draw (Cairo.Context ctx) {
var w = get_allocated_width ();
var h = get_allocated_height ();
var style = get_style_context ();
var border_radius = style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, style.get_state ()).get_int ();
if (cached != null) {
if (!cached.loading) {
Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf (cached.data, 1, null);
ctx.save ();
Drawing.draw_rounded_rect (ctx, 0, 0, w, h, border_radius);
//Proportionally scale to fit into the allocated container
var ratio = get_ratio (w, h);
ctx.scale (ratio, ratio);
//Center the result
var oh = cached.data.get_height ();
var result_h = oh*ratio;
var offset_y = (h - result_h) / 2;
var ow = cached.data.get_width ();
var result_w = ow*ratio;
var offset_x = (w - result_w) / 2;
ctx.translate (offset_x, offset_y);
//Draw it
ctx.set_source_surface (surface, 0, 0);
ctx.fill ();
ctx.restore ();
}
}
return false;
}
}

View File

@ -0,0 +1,68 @@
using Gtk;
using Gdk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/attachment_slot.ui")]
public class Tootle.Widgets.Attachment.Slot : FlowBoxChild {
[GtkChild]
EventBox event_box;
[GtkChild]
Label chip;
[GtkChild]
Image play_icon;
[GtkChild]
Stack stack;
public API.Attachment attachment { get; construct set; }
public Slot (API.Attachment obj) {
Object (attachment: obj);
if (attachment.preview_url != null) {
var img = new Widgets.Attachment.Picture (attachment.preview_url);
img.notify["visible"].connect (() => {
stack.visible_child_name = img.visible ? "content" : "loading";
});
stack.add_named (img, "content");
img.on_request ();
}
if (attachment.kind != "image") {
chip.label = attachment.kind;
chip.show ();
}
switch (attachment.kind) {
case "audio":
case "video":
case "gifv":
play_icon.show ();
break;
}
}
construct {
event_box.tooltip_text = attachment.description;
event_box.button_release_event.connect (on_clicked);
}
void download () {
Desktop.download (attachment.url, path => {
app.toast (_("Attachment downloaded"));
});
}
void open () {
Desktop.download (attachment.url, path => {
Desktop.open_uri (path);
});
}
protected virtual bool on_clicked (EventButton ev) {
if (ev.button != 1)
return false;
open ();
return true;
}
}

View File

@ -42,7 +42,6 @@ public class Tootle.Widgets.Avatar : EventBox {
public int get_scaled_size () {
return size; //return size * get_scale_factor ();
}
private void on_redraw () {
set_size_request (get_scaled_size (), get_scaled_size ());
@ -62,7 +61,7 @@ public class Tootle.Widgets.Avatar : EventBox {
}
else {
pixbuf = IconTheme.get_default ()
.load_icon_for_scale ("avatar-default", size, get_scale_factor (), IconLookupFlags.GENERIC_FALLBACK);
.load_icon_for_scale ("avatar-default", get_scaled_size (), get_scale_factor (), IconLookupFlags.GENERIC_FALLBACK);
}
Gdk.cairo_set_source_pixbuf (ctx, pixbuf, 0, 0);
ctx.fill ();