1/* 2 * Copyright 2018 Michael Gratton <mike@vee.net> 3 * 4 * This software is licensed under the GNU Lesser General Public License 5 * (version 2.1 or later). See the COPYING file in this distribution. 6 */ 7 8 9internal class Accounts.EditorRow<PaneType> : Gtk.ListBoxRow { 10 11 private const string DND_ATOM = "geary-editor-row"; 12 private const Gtk.TargetEntry[] DRAG_ENTRIES = { 13 { DND_ATOM, Gtk.TargetFlags.SAME_APP, 0 } 14 }; 15 16 17 protected Gtk.Grid layout { get; private set; default = new Gtk.Grid(); } 18 19 private Gtk.Container drag_handle; 20 private bool drag_picked_up = false; 21 private bool drag_entered = false; 22 23 24 public signal void move_to(int new_position); 25 public signal void dropped(EditorRow target); 26 27 28 public EditorRow() { 29 get_style_context().add_class("geary-settings"); 30 get_style_context().add_class("geary-labelled-row"); 31 32 this.layout.orientation = Gtk.Orientation.HORIZONTAL; 33 this.layout.show(); 34 add(this.layout); 35 36 // We'd like to add the drag handle only when needed, but 37 // GNOME/gtk#1495 prevents us from doing so. 38 Gtk.EventBox drag_box = new Gtk.EventBox(); 39 drag_box.add( 40 new Gtk.Image.from_icon_name( 41 "list-drag-handle-symbolic", Gtk.IconSize.BUTTON 42 ) 43 ); 44 this.drag_handle = new Gtk.Grid(); 45 this.drag_handle.valign = Gtk.Align.CENTER; 46 this.drag_handle.add(drag_box); 47 this.drag_handle.show_all(); 48 this.drag_handle.hide(); 49 // Translators: Tooltip for dragging list items 50 this.drag_handle.set_tooltip_text(_("Drag to move this item")); 51 this.layout.add(drag_handle); 52 53 this.show(); 54 } 55 56 public virtual void activated(PaneType pane) { 57 // No-op by default 58 } 59 60 public override bool key_press_event(Gdk.EventKey event) { 61 bool ret = Gdk.EVENT_PROPAGATE; 62 63 if (event.state == Gdk.ModifierType.CONTROL_MASK) { 64 int index = get_index(); 65 if (event.keyval == Gdk.Key.Up) { 66 index -= 1; 67 if (index >= 0) { 68 move_to(index); 69 ret = Gdk.EVENT_STOP; 70 } 71 } else if (event.keyval == Gdk.Key.Down) { 72 index += 1; 73 Gtk.ListBox? parent = get_parent() as Gtk.ListBox; 74 if (parent != null && 75 index < parent.get_children().length() && 76 !(parent.get_row_at_index(index) is AddRow)) { 77 move_to(index); 78 ret = Gdk.EVENT_STOP; 79 } 80 } 81 } 82 83 if (ret != Gdk.EVENT_STOP) { 84 ret = base.key_press_event(event); 85 } 86 87 return ret; 88 } 89 90 /** Adds a drag handle to the row and enables drag signals. */ 91 protected void enable_drag() { 92 Gtk.drag_source_set( 93 this.drag_handle, 94 Gdk.ModifierType.BUTTON1_MASK, 95 DRAG_ENTRIES, 96 Gdk.DragAction.MOVE 97 ); 98 99 Gtk.drag_dest_set( 100 this, 101 // No highlight, we'll take care of that ourselves so we 102 // can avoid highlighting the row that was picked up 103 Gtk.DestDefaults.MOTION | Gtk.DestDefaults.DROP, 104 DRAG_ENTRIES, 105 Gdk.DragAction.MOVE 106 ); 107 108 this.drag_handle.drag_begin.connect(on_drag_begin); 109 this.drag_handle.drag_end.connect(on_drag_end); 110 this.drag_handle.drag_data_get.connect(on_drag_data_get); 111 112 this.drag_motion.connect(on_drag_motion); 113 this.drag_leave.connect(on_drag_leave); 114 this.drag_data_received.connect(on_drag_data_received); 115 116 this.drag_handle.get_style_context().add_class("geary-drag-handle"); 117 this.drag_handle.show(); 118 119 get_style_context().add_class("geary-draggable"); 120 } 121 122 123 private void on_drag_begin(Gdk.DragContext context) { 124 // Draw a nice drag icon 125 Gtk.Allocation alloc = Gtk.Allocation(); 126 this.get_allocation(out alloc); 127 128 Cairo.ImageSurface surface = new Cairo.ImageSurface( 129 Cairo.Format.ARGB32, alloc.width, alloc.height 130 ); 131 Cairo.Context paint = new Cairo.Context(surface); 132 133 134 Gtk.StyleContext style = get_style_context(); 135 style.add_class("geary-drag-icon"); 136 draw(paint); 137 style.remove_class("geary-drag-icon"); 138 139 int x, y; 140 this.drag_handle.translate_coordinates(this, 0, 0, out x, out y); 141 surface.set_device_offset(-x, -y); 142 Gtk.drag_set_icon_surface(context, surface); 143 144 // Set a visual hint that the row is being dragged 145 style.add_class("geary-drag-source"); 146 this.drag_picked_up = true; 147 } 148 149 private void on_drag_end(Gdk.DragContext context) { 150 get_style_context().remove_class("geary-drag-source"); 151 this.drag_picked_up = false; 152 } 153 154 private bool on_drag_motion(Gdk.DragContext context, 155 int x, int y, 156 uint time_) { 157 if (!this.drag_entered) { 158 this.drag_entered = true; 159 160 // Don't highlight the same row that was picked up 161 if (!this.drag_picked_up) { 162 Gtk.ListBox? parent = get_parent() as Gtk.ListBox; 163 if (parent != null) { 164 parent.drag_highlight_row(this); 165 } 166 } 167 } 168 169 return true; 170 } 171 172 private void on_drag_leave(Gdk.DragContext context, 173 uint time_) { 174 if (!this.drag_picked_up) { 175 Gtk.ListBox? parent = get_parent() as Gtk.ListBox; 176 if (parent != null) { 177 parent.drag_unhighlight_row(); 178 } 179 } 180 this.drag_entered = false; 181 } 182 183 private void on_drag_data_get(Gdk.DragContext context, 184 Gtk.SelectionData selection_data, 185 uint info, uint time_) { 186 selection_data.set( 187 Gdk.Atom.intern_static_string(DND_ATOM), 8, 188 get_index().to_string().data 189 ); 190 } 191 192 private void on_drag_data_received(Gdk.DragContext context, 193 int x, int y, 194 Gtk.SelectionData selection_data, 195 uint info, uint time_) { 196 int drag_index = int.parse((string) selection_data.get_data()); 197 Gtk.ListBox? parent = this.get_parent() as Gtk.ListBox; 198 if (parent != null) { 199 EditorRow? drag_row = parent.get_row_at_index(drag_index) as EditorRow; 200 if (drag_row != null && drag_row != this) { 201 drag_row.dropped(this); 202 } 203 } 204 } 205 206} 207 208 209internal class Accounts.LabelledEditorRow<PaneType,V> : EditorRow<PaneType> { 210 211 212 public Gtk.Label label { get; private set; default = new Gtk.Label(""); } 213 public V value { get; private set; } 214 215 216 public LabelledEditorRow(string label, V value) { 217 this.label.halign = Gtk.Align.START; 218 this.label.valign = Gtk.Align.CENTER; 219 this.label.set_text(label); 220 this.label.show(); 221 this.layout.add(this.label); 222 223 bool expand_label = true; 224 this.value = value; 225 Gtk.Widget? widget = value as Gtk.Widget; 226 if (widget != null) { 227 Gtk.Entry? entry = value as Gtk.Entry; 228 if (entry != null) { 229 expand_label = false; 230 entry.xalign = 1; 231 entry.hexpand = true; 232 } 233 234 widget.valign = Gtk.Align.CENTER; 235 widget.show(); 236 this.layout.add(widget); 237 } 238 239 this.label.hexpand = expand_label; 240 } 241 242 public void set_dim_label(bool is_dim) { 243 if (is_dim) { 244 this.label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); 245 } else { 246 this.label.get_style_context().remove_class(Gtk.STYLE_CLASS_DIM_LABEL); 247 } 248 } 249 250} 251 252 253internal class Accounts.AddRow<PaneType> : EditorRow<PaneType> { 254 255 256 public AddRow() { 257 get_style_context().add_class("geary-add-row"); 258 Gtk.Image add_icon = new Gtk.Image.from_icon_name( 259 "list-add-symbolic", Gtk.IconSize.BUTTON 260 ); 261 add_icon.set_hexpand(true); 262 add_icon.show(); 263 264 this.layout.add(add_icon); 265 } 266 267} 268 269 270internal class Accounts.ServiceProviderRow<PaneType> : 271 LabelledEditorRow<PaneType,Gtk.Label> { 272 273 274 public ServiceProviderRow(Geary.ServiceProvider provider, 275 string other_type_label) { 276 string? label = null; 277 switch (provider) { 278 case Geary.ServiceProvider.GMAIL: 279 label = _("Gmail"); 280 break; 281 282 case Geary.ServiceProvider.OUTLOOK: 283 label = _("Outlook.com"); 284 break; 285 286 case Geary.ServiceProvider.YAHOO: 287 label = _("Yahoo"); 288 break; 289 290 case Geary.ServiceProvider.OTHER: 291 label = other_type_label; 292 break; 293 } 294 295 base( 296 // Translators: Label describes the service provider 297 // hosting the email account, e.g. Gmail, Yahoo, or some 298 // other generic IMAP service. 299 _("Service provider"), 300 new Gtk.Label(label) 301 ); 302 303 // Can't change this, so deactivate and dim out 304 set_activatable(false); 305 this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); 306 } 307 308} 309 310 311internal abstract class Accounts.AccountRow<PaneType,V> : 312 LabelledEditorRow<PaneType,V> { 313 314 315 internal Geary.AccountInformation account { get; private set; } 316 317 318 protected AccountRow(Geary.AccountInformation account, string label, V value) { 319 base(label, value); 320 this.account = account; 321 this.account.changed.connect(on_account_changed); 322 323 set_dim_label(true); 324 } 325 326 ~AccountRow() { 327 this.account.changed.disconnect(on_account_changed); 328 } 329 330 public abstract void update(); 331 332 private void on_account_changed() { 333 update(); 334 } 335 336} 337 338 339private abstract class Accounts.ServiceRow<PaneType,V> : AccountRow<PaneType,V> { 340 341 342 internal Geary.ServiceInformation service { get; private set; } 343 344 protected virtual bool is_value_editable { 345 get { 346 return ( 347 this.account.service_provider == Geary.ServiceProvider.OTHER && 348 !this.is_goa_account 349 ); 350 } 351 } 352 353 // XXX convenience method until we get a better way of doing this. 354 protected bool is_goa_account { 355 get { return (this.account.mediator is GoaMediator); } 356 } 357 358 359 protected ServiceRow(Geary.AccountInformation account, 360 Geary.ServiceInformation service, 361 string label, 362 V value) { 363 base(account, label, value); 364 this.service = service; 365 this.service.notify.connect_after(on_notify); 366 367 bool is_editable = this.is_value_editable; 368 set_activatable(is_editable); 369 370 Gtk.Widget? widget = value as Gtk.Widget; 371 if (widget != null && !is_editable) { 372 if (widget is Gtk.Label) { 373 widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); 374 } else { 375 widget.set_sensitive(false); 376 } 377 } 378 } 379 380 ~ServiceRow() { 381 this.service.notify.disconnect(on_notify); 382 } 383 384 private void on_notify() { 385 update(); 386 } 387 388} 389 390 391/** Interface for rows that use a validator for editable values. */ 392internal interface Accounts.ValidatingRow : EditorRow { 393 394 395 /** The row's validator */ 396 public abstract Components.Validator validator { get; protected set; } 397 398 /** Determines if the row's value has actually changed. */ 399 public abstract bool has_changed { get; } 400 401 /** Fired when validated and the value has actually changed. */ 402 public signal void changed(); 403 404 /** Fired when validated and the value has actually changed. */ 405 public signal void committed(); 406 407 /** 408 * Hooks up signals to the validator. 409 * 410 * Implementing classes should call this in their constructor 411 * after having constructed a validator 412 */ 413 protected void setup_validator() { 414 this.validator.changed.connect(on_validator_changed); 415 this.validator.activated.connect(on_validator_check_commit); 416 this.validator.focus_lost.connect(on_validator_check_commit); 417 } 418 419 /** 420 * Called when the row's value should be stored. 421 * 422 * This is only called when the row's value has changed, is 423 * valid, and the user has activated or changed to a different 424 * row. 425 */ 426 protected virtual void commit() { 427 // noop 428 } 429 430 private void on_validator_changed() { 431 if (this.has_changed) { 432 changed(); 433 } 434 } 435 436 private void on_validator_check_commit() { 437 if (this.has_changed) { 438 commit(); 439 committed(); 440 } 441 } 442 443} 444 445 446internal class Accounts.TlsComboBox : Gtk.ComboBox { 447 448 private const string INSECURE_ICON = "channel-insecure-symbolic"; 449 private const string SECURE_ICON = "channel-secure-symbolic"; 450 451 452 public string label { get; private set; default = ""; } 453 454 455 public Geary.TlsNegotiationMethod method { 456 get { 457 try { 458 return Geary.TlsNegotiationMethod.for_value(this.active_id); 459 } catch { 460 return Geary.TlsNegotiationMethod.TRANSPORT; 461 } 462 } 463 set { 464 this.active_id = value.to_value(); 465 } 466 } 467 468 469 public TlsComboBox() { 470 // Translators: This label describes what form of transport 471 // security (TLS, StartTLS, etc) used by an account's IMAP or SMTP 472 // service. 473 this.label = _("Connection security"); 474 475 Gtk.ListStore store = new Gtk.ListStore( 476 3, typeof(string), typeof(string), typeof(string) 477 ); 478 Gtk.TreeIter iter; 479 store.append(out iter); 480 store.set( 481 iter, 482 0, Geary.TlsNegotiationMethod.NONE.to_value(), 483 1, INSECURE_ICON, 484 2, _("None") 485 ); 486 store.append(out iter); 487 store.set( 488 iter, 489 0, Geary.TlsNegotiationMethod.START_TLS.to_value(), 490 1, SECURE_ICON, 491 2, _("StartTLS") 492 ); 493 store.append(out iter); 494 store.set( 495 iter, 496 0, Geary.TlsNegotiationMethod.TRANSPORT.to_value(), 497 1, SECURE_ICON, 498 2, _("TLS") 499 ); 500 501 this.model = store; 502 set_id_column(0); 503 504 Gtk.CellRendererText text_renderer = new Gtk.CellRendererText(); 505 pack_start(text_renderer, true); 506 add_attribute(text_renderer, "text", 2); 507 508 Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf(); 509 pack_start(icon_renderer, true); 510 add_attribute(icon_renderer, "icon_name", 1); 511 } 512 513} 514 515 516internal class Accounts.OutgoingAuthComboBox : Gtk.ComboBoxText { 517 518 519 public string label { get; private set; } 520 521 public Geary.Credentials.Requirement source { 522 get { 523 try { 524 return Geary.Credentials.Requirement.for_value(this.active_id); 525 } catch { 526 return Geary.Credentials.Requirement.USE_INCOMING; 527 } 528 } 529 set { 530 this.active_id = value.to_value(); 531 } 532 } 533 534 535 public OutgoingAuthComboBox() { 536 // Translators: Label for source of SMTP authentication 537 // credentials (none, use IMAP, custom) when adding a new 538 // account 539 this.label = _("Login"); 540 541 append( 542 Geary.Credentials.Requirement.NONE.to_value(), 543 // Translators: ComboBox value for source of SMTP 544 // authentication credentials (none) when adding a new 545 // account 546 _("No login needed") 547 ); 548 549 append( 550 Geary.Credentials.Requirement.USE_INCOMING.to_value(), 551 // Translators: ComboBox value for source of SMTP 552 // authentication credentials (use IMAP) when adding a new 553 // account 554 _("Use same login as receiving") 555 ); 556 557 append( 558 Geary.Credentials.Requirement.CUSTOM.to_value(), 559 // Translators: ComboBox value for source of SMTP 560 // authentication credentials (custom) when adding a new 561 // account 562 _("Use a different login") 563 ); 564 } 565 566} 567 568 569/** 570 * Displaying and manages validation of popover-based forms. 571 */ 572internal class Accounts.EditorPopover : Gtk.Popover { 573 574 575 internal Gtk.Grid layout { 576 get; private set; default = new Gtk.Grid(); 577 } 578 579 protected Gtk.Widget popup_focus = null; 580 581 582 public EditorPopover() { 583 get_style_context().add_class("geary-editor"); 584 585 this.layout.orientation = Gtk.Orientation.VERTICAL; 586 this.layout.set_row_spacing(6); 587 this.layout.set_column_spacing(12); 588 this.layout.show(); 589 add(this.layout); 590 591 this.closed.connect_after(on_closed); 592 } 593 594 ~EditorPopover() { 595 this.closed.disconnect(on_closed); 596 } 597 598 /** {@inheritDoc} */ 599 public new void popup() { 600 // Work-around GTK+ issue #1138 601 Gtk.Widget target = get_relative_to(); 602 603 Gtk.Allocation content_area; 604 target.get_allocation(out content_area); 605 606 Gtk.StyleContext style = target.get_style_context(); 607 Gtk.StateFlags flags = style.get_state(); 608 Gtk.Border margin = style.get_margin(flags); 609 610 content_area.x = margin.left; 611 content_area.y = margin.bottom; 612 content_area.width -= (content_area.x + margin.right); 613 content_area.height -= (content_area.y + margin.top); 614 615 set_pointing_to(content_area); 616 617 base.popup(); 618 619 if (this.popup_focus != null) { 620 this.popup_focus.grab_focus(); 621 } 622 } 623 624 public void add_labelled_row(string label, Gtk.Widget value) { 625 Gtk.Label label_widget = new Gtk.Label(label); 626 label_widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); 627 label_widget.halign = Gtk.Align.END; 628 label_widget.show(); 629 630 this.layout.add(label_widget); 631 this.layout.attach_next_to(value, label_widget, Gtk.PositionType.RIGHT); 632 } 633 634 private void on_closed() { 635 destroy(); 636 } 637 638} 639