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