1 /* 2 * gnote 3 * 4 * Copyright (C) 2011,2013-2014,2017,2019-2021 Aurimas Cernius 5 * Copyright (C) 2009 Hubert Figuiere 6 * 7 * This program is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 * 12 * This program is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with this program. If not, see <http://www.gnu.org/licenses/>. 19 */ 20 21 22 23 #include <gtk/gtk.h> 24 #include <gtkmm/image.h> 25 #include <gtkmm/linkbutton.h> 26 27 #include "sharp/xmlreader.hpp" 28 #include "sharp/xmlwriter.hpp" 29 #include "debug.hpp" 30 #include "notetag.hpp" 31 #include "noteeditor.hpp" 32 #include "utils.hpp" 33 34 namespace gnote { 35 NoteTag(const Glib::ustring & tag_name,int flags)36 NoteTag::NoteTag(const Glib::ustring & tag_name, int flags) 37 : Gtk::TextTag(tag_name) 38 , m_element_name(tag_name) 39 , m_widget(NULL) 40 , m_allow_middle_activate(false) 41 , m_flags(flags | CAN_SERIALIZE | CAN_SPLIT) 42 { 43 if (tag_name.empty()) { 44 throw sharp::Exception ("NoteTags must have a tag name. Use " 45 "DynamicNoteTag for constructing " 46 "anonymous tags."); 47 } 48 49 } 50 51 NoteTag()52 NoteTag::NoteTag() 53 : Gtk::TextTag() 54 , m_widget(NULL) 55 , m_allow_middle_activate(false) 56 , m_flags(0) 57 { 58 } 59 60 initialize(const Glib::ustring & element_name)61 void NoteTag::initialize(const Glib::ustring & element_name) 62 { 63 m_element_name = element_name; 64 m_flags = CAN_SERIALIZE | CAN_SPLIT; 65 m_save_type = CONTENT; 66 } 67 68 69 set_can_serialize(bool value)70 void NoteTag::set_can_serialize(bool value) 71 { 72 if (value) { 73 m_flags |= CAN_SERIALIZE; 74 } 75 else { 76 m_flags &= ~CAN_SERIALIZE; 77 } 78 } 79 set_can_undo(bool value)80 void NoteTag::set_can_undo(bool value) 81 { 82 if (value) { 83 m_flags |= CAN_UNDO; 84 } 85 else { 86 m_flags &= ~CAN_UNDO; 87 } 88 } 89 set_can_grow(bool value)90 void NoteTag::set_can_grow(bool value) 91 { 92 if (value) { 93 m_flags |= CAN_GROW; 94 } 95 else { 96 m_flags &= ~CAN_GROW; 97 } 98 } 99 set_can_spell_check(bool value)100 void NoteTag::set_can_spell_check(bool value) 101 { 102 if (value) { 103 m_flags |= CAN_SPELL_CHECK; 104 } 105 else { 106 m_flags &= ~CAN_SPELL_CHECK; 107 } 108 } 109 set_can_activate(bool value)110 void NoteTag::set_can_activate(bool value) 111 { 112 if (value) { 113 m_flags |= CAN_ACTIVATE; 114 } 115 else { 116 m_flags &= ~CAN_ACTIVATE; 117 } 118 } 119 set_can_split(bool value)120 void NoteTag::set_can_split(bool value) 121 { 122 if (value) { 123 m_flags |= CAN_SPLIT; 124 } 125 else { 126 m_flags &= ~CAN_SPLIT; 127 } 128 } 129 get_extents(const Gtk::TextIter & iter,Gtk::TextIter & start,Gtk::TextIter & end)130 void NoteTag::get_extents(const Gtk::TextIter & iter, Gtk::TextIter & start, 131 Gtk::TextIter & end) 132 { 133 Glib::RefPtr<Gtk::TextTag> this_ref = NoteTagTable::instance()->lookup(property_name()); 134 start = iter; 135 if(!start.starts_tag(this_ref)) { 136 start.backward_to_tag_toggle (this_ref); 137 } 138 end = iter; 139 end.forward_to_tag_toggle (this_ref); 140 } 141 write(sharp::XmlWriter & xml,bool start) const142 void NoteTag::write(sharp::XmlWriter & xml, bool start) const 143 { 144 if (can_serialize()) { 145 if (start) { 146 xml.write_start_element ("", m_element_name, ""); 147 } 148 else { 149 xml.write_end_element(); 150 } 151 } 152 } 153 read(sharp::XmlReader & xml,bool start)154 void NoteTag::read(sharp::XmlReader & xml, bool start) 155 { 156 if (can_serialize()) { 157 if (start) { 158 m_element_name = xml.get_name(); 159 } 160 } 161 } 162 on_event(const Glib::RefPtr<Glib::Object> & sender,GdkEvent * ev,const Gtk::TextIter & iter)163 bool NoteTag::on_event(const Glib::RefPtr<Glib::Object> & sender, GdkEvent *ev, const Gtk::TextIter & iter) 164 { 165 auto editor = dynamic_cast<NoteEditor*>(sender.get()); 166 Gtk::TextIter start, end; 167 168 if (!can_activate()) 169 return false; 170 171 switch (ev->type) { 172 case GDK_BUTTON_PRESS: 173 { 174 guint button; 175 gdk_event_get_button(ev, &button); 176 177 // Do not insert selected text when activating links with 178 // middle mouse button 179 if(button == 2) { 180 m_allow_middle_activate = true; 181 return true; 182 } 183 184 return false; 185 } 186 case GDK_BUTTON_RELEASE: 187 { 188 guint button; 189 gdk_event_get_button(ev, &button); 190 if((button != 1) && (button != 2)) 191 return false; 192 193 GdkModifierType state; 194 gdk_event_get_state(ev, &state); 195 /* Don't activate if Shift or Control is pressed */ 196 if((state & (Gdk::SHIFT_MASK | Gdk::CONTROL_MASK)) != 0) 197 return false; 198 199 // Prevent activation when selecting links with the mouse 200 if(editor && editor->get_buffer()->get_has_selection()) { 201 return false; 202 } 203 204 // Don't activate if the link has just been pasted with the 205 // middle mouse button (no preceding ButtonPress event) 206 if(button == 2 && !m_allow_middle_activate) { 207 return false; 208 } 209 else { 210 m_allow_middle_activate = false; 211 } 212 213 get_extents (iter, start, end); 214 if(editor) { 215 on_activate(*editor, start, end); 216 } 217 return false; 218 } 219 case GDK_KEY_PRESS: 220 { 221 GdkModifierType state; 222 gdk_event_get_state(ev, &state); 223 224 // Control-Enter activates the link at point... 225 if((state & Gdk::CONTROL_MASK) == 0) 226 return false; 227 228 guint keyval; 229 gdk_event_get_keyval(ev, &keyval); 230 if(keyval != GDK_KEY_Return && keyval != GDK_KEY_KP_Enter) 231 return false; 232 233 get_extents (iter, start, end); 234 if(editor) { 235 return on_activate(*editor, start, end); 236 } 237 } 238 default: 239 break; 240 } 241 242 return false; 243 } 244 245 on_activate(const NoteEditor & editor,const Gtk::TextIter & start,const Gtk::TextIter & end)246 bool NoteTag::on_activate(const NoteEditor & editor , const Gtk::TextIter & start, 247 const Gtk::TextIter & end) 248 { 249 bool retval = false; 250 251 #if 0 252 if (Activated != null) { 253 foreach (Delegate d in Activated.GetInvocationList()) { 254 TagActivatedHandler handler = (TagActivatedHandler) d; 255 retval |= handler (*this, editor, start, end); 256 } 257 } 258 #endif 259 retval = m_signal_activate(editor, start, end); 260 261 return retval; 262 } 263 264 get_image() const265 Glib::RefPtr<Gdk::Pixbuf> NoteTag::get_image() const 266 { 267 Gtk::Image * image = dynamic_cast<Gtk::Image*>(m_widget); 268 if(!image) { 269 return Glib::RefPtr<Gdk::Pixbuf>(); 270 } 271 return image->get_pixbuf(); 272 } 273 274 set_image(const Glib::RefPtr<Gdk::Pixbuf> & value)275 void NoteTag::set_image(const Glib::RefPtr<Gdk::Pixbuf> & value) 276 { 277 if(!value) { 278 set_widget(NULL); 279 return; 280 } 281 set_widget(new Gtk::Image(value)); 282 } 283 284 set_widget(Gtk::Widget * value)285 void NoteTag::set_widget(Gtk::Widget * value) 286 { 287 if ((value == NULL) && m_widget) { 288 delete m_widget; 289 } 290 291 m_widget = value; 292 293 try { 294 m_signal_changed(*this, false); 295 } catch (sharp::Exception & e) { 296 DBG_OUT("Exception calling TagChanged from NoteTag.set_Widget: %s", e.what()); 297 } 298 } 299 write(sharp::XmlWriter & xml,bool start) const300 void DynamicNoteTag::write(sharp::XmlWriter & xml, bool start) const 301 { 302 if (can_serialize()) { 303 NoteTag::write (xml, start); 304 305 if (start) { 306 for(AttributeMap::const_iterator iter = m_attributes.begin(); 307 iter != m_attributes.end(); ++iter) { 308 xml.write_attribute_string ("", iter->first, "", iter->second); 309 } 310 } 311 } 312 } 313 read(sharp::XmlReader & xml,bool start)314 void DynamicNoteTag::read(sharp::XmlReader & xml, bool start) 315 { 316 if (can_serialize()) { 317 NoteTag::read (xml, start); 318 319 if (start) { 320 while (xml.move_to_next_attribute()) { 321 Glib::ustring name = xml.get_name(); 322 323 xml.read_attribute_value(); 324 m_attributes [name] = xml.get_value(); 325 326 on_attribute_read (name); 327 DBG_OUT("NoteTag: %s read attribute %s='%s'", 328 get_element_name().c_str(), name.c_str(), xml.get_value().c_str()); 329 } 330 } 331 } 332 } 333 DepthNoteTag(int depth)334 DepthNoteTag::DepthNoteTag(int depth) 335 : NoteTag("depth:" + TO_STRING(depth) 336 + ":" + TO_STRING((int)Pango::DIRECTION_LTR)) 337 , m_depth(depth) 338 { 339 } 340 341 342 write(sharp::XmlWriter & xml,bool start) const343 void DepthNoteTag::write(sharp::XmlWriter & xml, bool start) const 344 { 345 if (can_serialize()) { 346 if (start) { 347 xml.write_start_element ("", "list-item", ""); 348 349 // Write the list items writing direction 350 xml.write_start_attribute ("dir"); 351 if (get_direction() == Pango::DIRECTION_RTL) { 352 xml.write_string ("rtl"); 353 } 354 else { 355 xml.write_string ("ltr"); 356 } 357 xml.write_end_attribute (); 358 } 359 else { 360 xml.write_end_element (); 361 } 362 } 363 } 364 365 366 NoteTagTable::Ptr NoteTagTable::s_instance; 367 _init_common_tags()368 void NoteTagTable::_init_common_tags() 369 { 370 NoteTag::Ptr tag; 371 Gdk::RGBA active_link_color, visited_link_color; 372 { 373 Gtk::LinkButton link; 374 active_link_color = link.get_style_context()->get_color(Gtk::STATE_FLAG_LINK); 375 visited_link_color = link.get_style_context()->get_color(Gtk::STATE_FLAG_VISITED); 376 } 377 378 // Font stylings 379 380 tag = NoteTag::create("centered", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 381 tag->property_justification() = Gtk::JUSTIFY_CENTER; 382 add (tag); 383 384 tag = NoteTag::create("bold", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 385 tag->property_weight() = PANGO_WEIGHT_BOLD; 386 add (tag); 387 388 tag = NoteTag::create("italic", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 389 tag->property_style() = Pango::STYLE_ITALIC; 390 add (tag); 391 392 tag = NoteTag::create("strikethrough", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 393 tag->property_strikethrough() = true; 394 add (tag); 395 396 tag = NoteTag::create("highlight", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 397 tag->property_background() = "yellow"; 398 add (tag); 399 400 tag = NoteTag::create("find-match", NoteTag::CAN_SPELL_CHECK); 401 tag->property_background() = "green"; 402 tag->set_can_serialize(false); 403 tag->set_save_type(META); 404 add (tag); 405 406 tag = NoteTag::create("note-title", 0); 407 tag->property_foreground_rgba().set_value(active_link_color); 408 tag->property_scale() = Pango::SCALE_XX_LARGE; 409 // FiXME: Hack around extra rewrite on open 410 tag->set_can_serialize(false); 411 tag->set_save_type(META); 412 add (tag); 413 414 tag = NoteTag::create("related-to", 0); 415 tag->property_scale() = Pango::SCALE_SMALL; 416 tag->property_left_margin() = 40; 417 tag->property_editable() = false; 418 tag->set_save_type(META); 419 add (tag); 420 421 // Used when inserting dropped URLs/text to Start Here 422 tag = NoteTag::create("datetime", 0); 423 tag->property_scale() = Pango::SCALE_SMALL; 424 tag->property_style() = Pango::STYLE_ITALIC; 425 tag->property_foreground_rgba().set_value(visited_link_color); 426 tag->set_save_type(META); 427 add (tag); 428 429 // Font sizes 430 431 tag = NoteTag::create("size:huge", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 432 tag->property_scale() = Pango::SCALE_XX_LARGE; 433 add (tag); 434 435 tag = NoteTag::create("size:large", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 436 tag->property_scale() = Pango::SCALE_X_LARGE; 437 add (tag); 438 439 tag = NoteTag::create("size:normal", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 440 tag->property_scale() = Pango::SCALE_MEDIUM; 441 add (tag); 442 443 tag = NoteTag::create("size:small", NoteTag::CAN_UNDO | NoteTag::CAN_GROW | NoteTag::CAN_SPELL_CHECK); 444 tag->property_scale() = Pango::SCALE_SMALL; 445 add (tag); 446 447 // Links 448 449 tag = NoteTag::create("link:broken", NoteTag::CAN_ACTIVATE); 450 tag->property_underline() = Pango::UNDERLINE_SINGLE; 451 tag->property_foreground_rgba().set_value(visited_link_color); 452 tag->set_save_type(META); 453 add (tag); 454 m_broken_link_tag = tag; 455 456 tag = NoteTag::create("link:internal", NoteTag::CAN_ACTIVATE); 457 tag->property_underline() = Pango::UNDERLINE_SINGLE; 458 tag->property_foreground_rgba().set_value(active_link_color); 459 tag->set_save_type(META); 460 add (tag); 461 m_link_tag = tag; 462 463 tag = NoteTag::create("link:url", NoteTag::CAN_ACTIVATE); 464 tag->property_underline() = Pango::UNDERLINE_SINGLE; 465 tag->property_foreground_rgba().set_value(active_link_color); 466 tag->set_save_type(META); 467 add (tag); 468 m_url_tag = tag; 469 } 470 471 tag_is_serializable(const Glib::RefPtr<const Gtk::TextTag> & tag)472 bool NoteTagTable::tag_is_serializable(const Glib::RefPtr<const Gtk::TextTag> & tag) 473 { 474 NoteTag::ConstPtr note_tag = NoteTag::ConstPtr::cast_dynamic(tag); 475 if(note_tag) { 476 return note_tag->can_serialize(); 477 } 478 return false; 479 } 480 tag_is_growable(const Glib::RefPtr<Gtk::TextTag> & tag)481 bool NoteTagTable::tag_is_growable(const Glib::RefPtr<Gtk::TextTag> & tag) 482 { 483 NoteTag::Ptr note_tag = NoteTag::Ptr::cast_dynamic(tag); 484 if(note_tag) { 485 return note_tag->can_grow(); 486 } 487 return false; 488 } 489 tag_is_undoable(const Glib::RefPtr<Gtk::TextTag> & tag)490 bool NoteTagTable::tag_is_undoable(const Glib::RefPtr<Gtk::TextTag> & tag) 491 { 492 NoteTag::Ptr note_tag = NoteTag::Ptr::cast_dynamic(tag); 493 if(note_tag) { 494 return note_tag->can_undo(); 495 } 496 return false; 497 } 498 499 tag_is_spell_checkable(const Glib::RefPtr<const Gtk::TextTag> & tag)500 bool NoteTagTable::tag_is_spell_checkable(const Glib::RefPtr<const Gtk::TextTag> & tag) 501 { 502 NoteTag::ConstPtr note_tag = NoteTag::ConstPtr::cast_dynamic(tag); 503 if(note_tag) { 504 return note_tag->can_spell_check(); 505 } 506 return false; 507 } 508 509 tag_is_activatable(const Glib::RefPtr<Gtk::TextTag> & tag)510 bool NoteTagTable::tag_is_activatable(const Glib::RefPtr<Gtk::TextTag> & tag) 511 { 512 NoteTag::Ptr note_tag = NoteTag::Ptr::cast_dynamic(tag); 513 if(note_tag) { 514 return note_tag->can_activate(); 515 } 516 return false; 517 } 518 519 tag_has_depth(const Glib::RefPtr<Gtk::TextBuffer::Tag> & tag)520 bool NoteTagTable::tag_has_depth(const Glib::RefPtr<Gtk::TextBuffer::Tag> & tag) 521 { 522 return (bool)DepthNoteTag::Ptr::cast_dynamic(tag); 523 } 524 525 has_link_tag(const Gtk::TextIter & iter)526 bool NoteTagTable::has_link_tag(const Gtk::TextIter & iter) 527 { 528 return iter.has_tag(get_link_tag()) || iter.has_tag(get_url_tag()) || iter.has_tag(get_broken_link_tag()); 529 } 530 531 get_change_type(const Glib::RefPtr<Gtk::TextTag> & tag)532 ChangeType NoteTagTable::get_change_type(const Glib::RefPtr<Gtk::TextTag> &tag) 533 { 534 ChangeType change; 535 536 // Use tag Name for Gtk.TextTags 537 // For extensibility, add Gtk.TextTag names here 538 change = OTHER_DATA_CHANGED; 539 540 // Use SaveType for NoteTags 541 Glib::RefPtr<NoteTag> note_tag = Glib::RefPtr<NoteTag>::cast_dynamic(tag); 542 if(note_tag) { 543 switch(note_tag->save_type()) { 544 case META: 545 change = OTHER_DATA_CHANGED; 546 break; 547 case CONTENT: 548 change = CONTENT_CHANGED; 549 break; 550 case NO_SAVE: 551 default: 552 change = NO_CHANGE; 553 break; 554 } 555 } 556 557 return change; 558 } 559 560 get_depth_tag(int depth)561 DepthNoteTag::Ptr NoteTagTable::get_depth_tag(int depth) 562 { 563 Glib::ustring name = "depth:" + TO_STRING(depth) + ":" + TO_STRING((int)Pango::DIRECTION_LTR); 564 565 DepthNoteTag::Ptr tag = DepthNoteTag::Ptr::cast_dynamic(lookup(name)); 566 567 if (!tag) { 568 tag = DepthNoteTag::Ptr(new DepthNoteTag(depth)); 569 tag->property_indent().set_value(-14); 570 tag->property_left_margin().set_value((depth+1) * 25); 571 tag->property_pixels_below_lines().set_value(4); 572 tag->property_scale().set_value(Pango::SCALE_MEDIUM); 573 add (tag); 574 } 575 576 return tag; 577 } 578 create_dynamic_tag(const Glib::ustring & tag_name)579 DynamicNoteTag::Ptr NoteTagTable::create_dynamic_tag(const Glib::ustring & tag_name) 580 { 581 auto iter = m_tag_types.find(tag_name); 582 if(iter == m_tag_types.end()) { 583 return DynamicNoteTag::Ptr(); 584 } 585 DynamicNoteTag::Ptr tag(iter->second()); 586 tag->initialize(tag_name); 587 add(tag); 588 return tag; 589 } 590 591 register_dynamic_tag(const Glib::ustring & tag_name,const Factory & factory)592 void NoteTagTable::register_dynamic_tag(const Glib::ustring & tag_name, const Factory & factory) 593 { 594 m_tag_types[tag_name] = factory; 595 } 596 597 is_dynamic_tag_registered(const Glib::ustring & tag_name)598 bool NoteTagTable::is_dynamic_tag_registered(const Glib::ustring & tag_name) 599 { 600 return m_tag_types.find(tag_name) != m_tag_types.end(); 601 } 602 on_tag_added(const Glib::RefPtr<Gtk::TextTag> & tag)603 void NoteTagTable::on_tag_added(const Glib::RefPtr<Gtk::TextTag> & tag) 604 { 605 m_added_tags.push_back(tag); 606 607 NoteTag::Ptr note_tag = NoteTag::Ptr::cast_dynamic(tag); 608 if (note_tag) { 609 // note_tag->signal_changed().connect(sigc::mem_fun(*this, &NoteTagTable::on_notetag_changed)); 610 } 611 } 612 613 on_tag_removed(const Glib::RefPtr<Gtk::TextTag> & tag)614 void NoteTagTable::on_tag_removed(const Glib::RefPtr<Gtk::TextTag> & tag) 615 { 616 utils::remove_swap_back(m_added_tags, tag); 617 618 NoteTag::Ptr note_tag = NoteTag::Ptr::cast_dynamic(tag); 619 if (note_tag) { 620 // TODO disconnect the signal 621 // note_tag.Changed -= OnTagChanged; 622 } 623 } 624 625 626 #if 0 627 void NoteTagTable::on_notetag_changed(Glib::RefPtr<Gtk::TextTag>& tag, bool size_changed) 628 { 629 m_signal_changed(tag, size_changed); 630 } 631 #endif 632 633 } 634 635