MarkdownEditor.vala 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. using Gtk;
  2. using Adw;
  3. using GtkCommonMark;
  4. namespace Publicate.Editors {
  5. public class MarkdownEditor : Box, EditorWidget, Savable {
  6. private Paned paned_view;
  7. private Box toolbar;
  8. private GtkCommonMark.MarkdownView? markdown_view = null;
  9. private GtkSource.View text_view;
  10. private GtkSource.Buffer source_buffer;
  11. private GtkSource.LanguageManager language_manager;
  12. private GtkSource.StyleSchemeManager style_manager;
  13. private ScrolledWindow source_scroller;
  14. private ScrolledWindow? markdown_scroller = null;
  15. private ViewerWindow window;
  16. private TabPage page;
  17. private DropDown style_selector;
  18. private ToggleButton bold_button;
  19. private ToggleButton italic_button;
  20. private Button insert_link;
  21. private Button insert_image;
  22. private ToggleButton preview_button;
  23. private const string STYLE_HEADING_1 = "Heading 1";
  24. private const string STYLE_HEADING_2 = "Heading 2";
  25. private const string STYLE_HEADING_3 = "Heading 3";
  26. private const string STYLE_HEADING_4 = "Heading 4";
  27. private const string STYLE_HEADING_5 = "Heading 5";
  28. private const string STYLE_HEADING_6 = "Heading 6";
  29. private const string STYLE_QUOTATION = "Quotation";
  30. private const string STYLE_PREFORMATTED = "Preformatted";
  31. private const string STYLE_PARAGRAPH = "Paragraph";
  32. private Spelling.Checker spell_checker;
  33. private Spelling.TextBufferAdapter spell_adapter;
  34. private bool inhibit_toolbar_updates = false;
  35. private Gee.HashMap<string, Gdk.Pixbuf> pixbuf_cache = new Gee.HashMap<string, Gdk.Pixbuf>();
  36. public Adw.TabPage tab_page { get {
  37. return page;
  38. } }
  39. public string asset_name { get{
  40. return asset.name;
  41. } }
  42. private bool unsaved = false;
  43. public bool has_unsaved_changes { get{
  44. return unsaved;
  45. } }
  46. private Ppub.Asset asset;
  47. public MarkdownEditor(ViewerWindow win, TabView tab_view) {
  48. window = win;
  49. orientation = Orientation.VERTICAL;
  50. source_scroller = new ScrolledWindow ();
  51. paned_view = new Paned(Orientation.HORIZONTAL);
  52. text_view = new GtkSource.View ();
  53. language_manager = new GtkSource.LanguageManager ();
  54. style_manager = new GtkSource.StyleSchemeManager();
  55. text_view.monospace = true;
  56. text_view.show_line_numbers = true;
  57. text_view.auto_indent = true;
  58. source_buffer = (GtkSource.Buffer) text_view.buffer;
  59. language_manager.append_search_path("/usr/share/publicate/gtk-source-view");
  60. source_buffer.language = language_manager.get_language("ppub-markdown");
  61. text_view.hexpand = true;
  62. text_view.set_wrap_mode (WrapMode.WORD_CHAR);
  63. text_view.top_margin = 18;
  64. text_view.bottom_margin = 18;
  65. text_view.left_margin = 18;
  66. text_view.right_margin = 18;
  67. text_view.buffer.changed.connect (() => update_preview());
  68. spell_checker = Spelling.Checker.get_default();
  69. spell_adapter = new Spelling.TextBufferAdapter(source_buffer, spell_checker);
  70. text_view.set_extra_menu(spell_adapter.get_menu_model());
  71. text_view.insert_action_group("spelling", spell_adapter);
  72. spell_adapter.set_enabled(true);
  73. source_scroller.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
  74. source_scroller.vexpand = true;
  75. source_scroller.vadjustment.value_changed.connect(sync_scroll);
  76. source_scroller.child = text_view;
  77. var source_box = new Box(Orientation.VERTICAL, 0);
  78. source_box.append(source_scroller);
  79. paned_view.start_child = source_box;
  80. Gtk.Settings.get_default().notify["gtk-application-prefer-dark-theme"].connect(() => configure_tags());
  81. configure_tags();
  82. toolbar = new Box(Orientation.HORIZONTAL, 8);
  83. toolbar.margin_start = 8;
  84. toolbar.margin_end = 8;
  85. toolbar.margin_top = 8;
  86. toolbar.margin_bottom = 8;
  87. style_selector = new DropDown.from_strings(new string[] {STYLE_HEADING_1, STYLE_HEADING_2, STYLE_HEADING_3, STYLE_HEADING_4, STYLE_HEADING_5, STYLE_HEADING_6, STYLE_QUOTATION, STYLE_PREFORMATTED, STYLE_PARAGRAPH});
  88. toolbar.append(style_selector);
  89. bold_button = new ToggleButton();
  90. bold_button.child = new Image.from_icon_name("format-text-bold-symbolic");
  91. bold_button.tooltip_text = "Bold";
  92. bold_button.clicked.connect(() => toggle_bold());
  93. toolbar.append(bold_button);
  94. italic_button = new ToggleButton();
  95. italic_button.child = new Image.from_icon_name("format-text-italic-symbolic");
  96. italic_button.tooltip_text = "Italic";
  97. italic_button.clicked.connect(() => toggle_italic());
  98. toolbar.append(italic_button);
  99. insert_link = new Button.from_icon_name("insert-link-symbolic");
  100. insert_link.tooltip_text = "Insert link";
  101. insert_link.hexpand = true;
  102. insert_link.halign = Align.END;
  103. insert_link.clicked.connect(() => insert_link_clicked());
  104. toolbar.append(insert_link);
  105. insert_image = new Button.from_icon_name("insert-image-symbolic");
  106. insert_image.tooltip_text = "Insert image";
  107. insert_image.clicked.connect(() => insert_image_clicked());
  108. toolbar.append(insert_image);
  109. preview_button = new ToggleButton();
  110. preview_button.child = new Image.from_icon_name("view-dual-symbolic");
  111. preview_button.tooltip_text = "Markdown Preview";
  112. preview_button.active = true;
  113. preview_button.clicked.connect(() => toggle_preview());
  114. toolbar.append(preview_button);
  115. source_box.append(toolbar);
  116. source_buffer.cursor_moved.connect(() => update_toolbar());
  117. source_buffer.highlight_updated.connect(() => update_toolbar());
  118. append(paned_view);
  119. initialise_preview();
  120. page = tab_view.add_page (this, null);
  121. }
  122. private void initialise_preview() {
  123. markdown_scroller = new ScrolledWindow ();
  124. markdown_scroller.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
  125. markdown_scroller.vexpand = true;
  126. markdown_view = new GtkCommonMark.MarkdownView ();
  127. markdown_view.hexpand = true;
  128. markdown_view.set_wrap_mode (WrapMode.WORD_CHAR);
  129. markdown_view.widget_embedded.connect(widget_embedded);
  130. markdown_scroller.child = markdown_view;
  131. // markdown_scroller.hadjustment.changed.connect(() => sync_scroll());
  132. paned_view.end_child = markdown_scroller;
  133. set_leaflet_size();
  134. configure_tags();
  135. }
  136. private void destruct_preview() {
  137. paned_view.end_child = null;
  138. markdown_scroller = null;
  139. markdown_view = null;
  140. }
  141. public void set_zoom_percentage (int percent) {
  142. if(markdown_view != null) {
  143. var scale = (float)percent / 100f;
  144. markdown_view.tag_manager.font_scale = scale;
  145. }
  146. }
  147. public async void load_asset (Ppub.Publication publication, Ppub.Asset asset) {
  148. if(markdown_view != null) {
  149. markdown_view.buffer.set_text("", 0);
  150. }
  151. page.title = asset.name;
  152. this.asset = asset;
  153. text_view.buffer.set_text("", 0);
  154. MemoryOutputStream os = new MemoryOutputStream (null, GLib.realloc, GLib.free);
  155. yield os.splice_async (publication.read_asset (asset.name), OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET);
  156. var text = os.steal_data ();
  157. text.length = (int) os.get_data_size ();
  158. text_view.buffer.set_text ((string)text, text.length);
  159. set_tab_name(false);
  160. Timeout.add_once(100, () => set_leaflet_size());
  161. }
  162. private void set_leaflet_size() {
  163. paned_view.position = int.max(300, paned_view.get_width() / 2);
  164. }
  165. public void save_asset(Ppub.Builder builder) {
  166. var article_data = get_text().to_utf8();
  167. var article_stream = new MemoryInputStream.from_data((uint8[])article_data);
  168. var compression_info = new Ppub.CompressionInfo(article_stream);
  169. article_stream.seek(0, SeekType.SET);
  170. builder.add_asset(asset.name, asset.mimetype, article_stream, Invercargill.Iterate.nothing<string>(), compression_info);
  171. set_tab_name(false);
  172. }
  173. protected async void widget_embedded(GtkCommonMark.MarkdownViewEmbeddedWidgetHost widget, string file, string title) {
  174. var image = new Gtk.Picture();
  175. image.content_fit = Gtk.ContentFit.FILL;
  176. widget.append (image);
  177. Gdk.Pixbuf pixbuf;
  178. if(pixbuf_cache.has_key(file)) {
  179. pixbuf = pixbuf_cache[file];
  180. }
  181. else {
  182. pixbuf = yield new Gdk.Pixbuf.from_stream_async (window.publication.read_asset (file), null);
  183. pixbuf_cache.set(file, pixbuf);
  184. }
  185. image.set_pixbuf (pixbuf);
  186. widget.available_width_changed.connect(wid => {
  187. image.width_request = wid;
  188. var ratio = image.get_paintable().get_intrinsic_aspect_ratio ();
  189. image.height_request = (int)(wid / ratio);
  190. });
  191. image.destroy.connect(() => print("IT gone\n"));
  192. }
  193. private void configure_tags() {
  194. var link = new LinkButton("");
  195. if(markdown_view != null) {
  196. markdown_view.tag_manager.update_link_colour(link.get_color());
  197. }
  198. if(Gtk.Settings.get_default().gtk_application_prefer_dark_theme) {
  199. source_buffer.style_scheme = style_manager.get_scheme("Adwaita-dark");
  200. }
  201. else {
  202. source_buffer.style_scheme = style_manager.get_scheme("Adwaita");
  203. }
  204. }
  205. private void update_preview(bool changes = true) {
  206. if(changes) {
  207. set_tab_name(true);
  208. }
  209. if(markdown_view == null) {
  210. return;
  211. }
  212. var text = get_text();
  213. markdown_view.buffer.set_text("", 0);
  214. markdown_view.load_from_string((string)text);
  215. }
  216. public string get_text() {
  217. TextIter start_iter;
  218. TextIter end_iter;
  219. text_view.buffer.get_start_iter (out start_iter);
  220. text_view.buffer.get_end_iter (out end_iter);
  221. var text = text_view.buffer.get_text (start_iter, end_iter, true);
  222. return text;
  223. }
  224. private void sync_scroll() {
  225. if(markdown_scroller != null) {
  226. set_scroll_from_frac(markdown_scroller, get_scroll_frac(source_scroller));
  227. }
  228. }
  229. private static double get_scroll_frac(ScrolledWindow scroller) {
  230. return (scroller.vadjustment.value/(scroller.vadjustment.upper - scroller.vadjustment.page_size));
  231. }
  232. private void set_scroll_from_frac(ScrolledWindow scroller, double frac) {
  233. scroller.vadjustment.value = (scroller.vadjustment.upper - scroller.vadjustment.page_size)*frac;
  234. }
  235. private void set_tab_name(bool unsaved) {
  236. this.unsaved = unsaved;
  237. if(unsaved) {
  238. tab_page.title = @"⏺ $(asset.name)";
  239. }
  240. else {
  241. tab_page.title = asset.name;
  242. }
  243. }
  244. private void update_toolbar() {
  245. if(inhibit_toolbar_updates) {
  246. print("Toolbar update inhibited\n");
  247. return;
  248. }
  249. TextIter iter_start;
  250. TextIter iter_end;
  251. source_buffer.get_iter_at_offset(out iter_start, source_buffer.cursor_position);
  252. var multi_line = false;
  253. if(source_buffer.has_selection) {
  254. source_buffer.get_selection_bounds(out iter_start, out iter_end);
  255. multi_line = (iter_start.get_line() != iter_end.get_line());
  256. }
  257. var classes = Invercargill.Wrap.array<string>(source_buffer.get_context_classes_at_iter(iter_start));
  258. // Disable formatting changes if the selection is over many lines or the "no-format" class is applied
  259. toolbar.sensitive = !multi_line && !classes.any(c => c == "no-format");
  260. print(@"$(classes.to_string(s => s, " "))\n");
  261. set_line_style_type_in_dropdown(get_line_style_from_buffer());
  262. bold_button.active = classes.any(c => c == "b");
  263. italic_button.active = classes.any(c => c == "i");
  264. }
  265. private int get_line_style_from_buffer() {
  266. TextIter iter;
  267. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  268. if(iter.ends_line()) {
  269. iter.backward_char();
  270. }
  271. var classes = Invercargill.Wrap.array<string>(source_buffer.get_context_classes_at_iter(iter));
  272. var heading = classes.first_or_default(c => c.has_prefix("h") && c.length == 2);
  273. if(heading != null) {
  274. var level = int.parse(heading[1:2]);
  275. return level - 1;
  276. }
  277. else if(classes.any(c => c == "quot")) {
  278. return 6;
  279. }
  280. else if(classes.any(c => c == "pre")) {
  281. return 7;
  282. }
  283. return 8;
  284. }
  285. private void remove_line_style() {
  286. TextIter iter;
  287. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  288. if(iter.ends_line()) {
  289. iter.backward_char();
  290. }
  291. var classes = Invercargill.Wrap.array<string>(source_buffer.get_context_classes_at_iter(iter));
  292. var line_no = iter.get_line();
  293. var heading = classes.first_or_default(c => c.has_prefix("h") && c.length == 2);
  294. if(heading != null) {
  295. var level = int.parse(heading[1:2]);
  296. source_buffer.get_iter_at_line(out iter, line_no);
  297. iter.forward_chars(level+1);
  298. for(int i = 0; i <= level; i++) {
  299. source_buffer.backspace(ref iter, false, true);
  300. }
  301. }
  302. else if(classes.any(c => c == "quot")) {
  303. source_buffer.get_iter_at_line(out iter, line_no);
  304. iter.forward_chars(2);
  305. source_buffer.backspace(ref iter, false, true);
  306. source_buffer.backspace(ref iter, false, true);
  307. }
  308. }
  309. private void set_line_style_type_in_dropdown(uint type) {
  310. style_selector.notify["selected"].disconnect(set_line_style);
  311. style_selector.selected = type;
  312. style_selector.notify["selected"].connect(set_line_style);
  313. }
  314. private void set_line_style() {
  315. var style = style_selector.selected;
  316. inhibit_toolbar_updates = true;
  317. source_buffer.begin_user_action();
  318. remove_line_style();
  319. print(@"Set style $style\n");
  320. if(style < 6) {
  321. // Headings
  322. format_line_heading(style + 1);
  323. }
  324. else if(style == 6) {
  325. format_line_quote();
  326. }
  327. else if(style == 7) {
  328. format_line_preformatted();
  329. }
  330. set_line_style_type_in_dropdown(style);
  331. inhibit_toolbar_updates = false;
  332. source_buffer.end_user_action();
  333. }
  334. private void format_line_heading(uint level) {
  335. TextIter iter;
  336. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  337. var line_no = iter.get_line();
  338. source_buffer.get_iter_at_line(out iter, line_no);
  339. for(var i = 0; i < level; i++) {
  340. source_buffer.insert(ref iter, "#", 1);
  341. }
  342. source_buffer.insert(ref iter, " ", 1);
  343. iter.forward_line();
  344. iter.backward_char();
  345. source_buffer.place_cursor(iter);
  346. }
  347. private void format_line_quote() {
  348. TextIter iter;
  349. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  350. var line_no = iter.get_line();
  351. source_buffer.get_iter_at_line(out iter, line_no);
  352. source_buffer.insert(ref iter, "> ", 2);
  353. iter.forward_line();
  354. iter.backward_char();
  355. source_buffer.place_cursor(iter);
  356. }
  357. private void format_line_preformatted() {
  358. TextIter iter;
  359. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  360. var line_no = iter.get_line();
  361. source_buffer.get_iter_at_line(out iter, line_no);
  362. source_buffer.insert(ref iter, "```\n", 4);
  363. iter.forward_line();
  364. iter.backward_char();
  365. source_buffer.insert(ref iter, "\n```", 4);
  366. iter.forward_line();
  367. iter.backward_chars(5);
  368. source_buffer.place_cursor(iter);
  369. }
  370. private void format_text_bold() {
  371. TextIter iter_start;
  372. TextIter iter_end;
  373. if(source_buffer.has_selection) {
  374. source_buffer.get_selection_bounds(out iter_start, out iter_end);
  375. }
  376. else {
  377. source_buffer.get_iter_at_offset(out iter_start, source_buffer.cursor_position);
  378. source_buffer.get_iter_at_offset(out iter_end, source_buffer.cursor_position);
  379. if(!iter_start.starts_word()) {
  380. iter_start.backward_word_start();
  381. }
  382. if(!iter_end.ends_word()) {
  383. iter_end.forward_word_end();
  384. }
  385. }
  386. var difference = iter_end.get_offset() - iter_start.get_offset();
  387. source_buffer.insert(ref iter_start, "**", 2);
  388. iter_start.forward_chars(difference);
  389. source_buffer.insert(ref iter_start, "**", 2);
  390. }
  391. private void format_text_unbold() {
  392. TextIter iter;
  393. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  394. source_buffer.iter_forward_to_context_class_toggle(ref iter, "b");
  395. var end_pos = iter.get_offset();
  396. source_buffer.iter_backward_to_context_class_toggle(ref iter, "b");
  397. iter.forward_chars(2);
  398. source_buffer.backspace(ref iter, false, true);
  399. source_buffer.backspace(ref iter, false, true);
  400. iter.set_offset(end_pos-2);
  401. source_buffer.backspace(ref iter, false, true);
  402. source_buffer.backspace(ref iter, false, true);
  403. }
  404. private void toggle_bold() {
  405. source_buffer.begin_user_action();
  406. if(!bold_button.active) {
  407. format_text_unbold();
  408. }
  409. else {
  410. format_text_bold();
  411. }
  412. source_buffer.end_user_action();
  413. }
  414. private void format_text_italic() {
  415. TextIter iter_start;
  416. TextIter iter_end;
  417. if(source_buffer.has_selection) {
  418. source_buffer.get_selection_bounds(out iter_start, out iter_end);
  419. }
  420. else {
  421. source_buffer.get_iter_at_offset(out iter_start, source_buffer.cursor_position);
  422. source_buffer.get_iter_at_offset(out iter_end, source_buffer.cursor_position);
  423. if(!iter_start.starts_word()) {
  424. iter_start.backward_word_start();
  425. }
  426. if(!iter_end.ends_word()) {
  427. iter_end.forward_word_end();
  428. }
  429. }
  430. var difference = iter_end.get_offset() - iter_start.get_offset();
  431. source_buffer.insert(ref iter_start, "*", 1);
  432. iter_start.forward_chars(difference);
  433. source_buffer.insert(ref iter_start, "*", 1);
  434. }
  435. private void format_text_unitalic() {
  436. TextIter iter;
  437. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  438. source_buffer.iter_forward_to_context_class_toggle(ref iter, "i");
  439. var end_pos = iter.get_offset();
  440. source_buffer.iter_backward_to_context_class_toggle(ref iter, "i");
  441. iter.forward_chars(1);
  442. source_buffer.backspace(ref iter, false, true);
  443. iter.set_offset(end_pos-1);
  444. source_buffer.backspace(ref iter, false, true);
  445. }
  446. private void toggle_italic() {
  447. source_buffer.begin_user_action();
  448. if(!italic_button.active) {
  449. format_text_unitalic();
  450. }
  451. else {
  452. format_text_italic();
  453. }
  454. source_buffer.end_user_action();
  455. }
  456. private void insert_link_clicked() {
  457. string selected = null;
  458. if(source_buffer.has_selection) {
  459. TextIter iter_start;
  460. TextIter iter_end;
  461. source_buffer.get_selection_bounds(out iter_start, out iter_end);
  462. selected = iter_start.get_text(iter_end);
  463. }
  464. var win = new AddLinkWindow(selected);
  465. win.insert_link.connect(link_insert);
  466. win.set_transient_for(window);
  467. win.modal = true;
  468. win.present();
  469. }
  470. private void link_insert(string url, string? label) {
  471. TextIter iter;
  472. source_buffer.begin_user_action();
  473. if(source_buffer.has_selection) {
  474. TextIter iter_end;
  475. source_buffer.get_selection_bounds(out iter, out iter_end);
  476. source_buffer.delete(ref iter, ref iter_end);
  477. }
  478. else {
  479. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  480. }
  481. var link = "";
  482. if(label != null) {
  483. link = @"[$label]($url)";
  484. }
  485. else {
  486. link = @"<$url>";
  487. }
  488. source_buffer.insert(ref iter, link, link.length);
  489. source_buffer.end_user_action();
  490. }
  491. private void insert_image_clicked() {
  492. string selected = null;
  493. if(source_buffer.has_selection) {
  494. TextIter iter_start;
  495. TextIter iter_end;
  496. source_buffer.get_selection_bounds(out iter_start, out iter_end);
  497. selected = iter_start.get_text(iter_end);
  498. }
  499. var win = new AddImageWindow(selected, window);
  500. win.insert_image.connect(image_insert);
  501. win.set_transient_for(window);
  502. win.modal = true;
  503. win.present();
  504. }
  505. private void image_insert(string url, string label) {
  506. TextIter iter;
  507. source_buffer.begin_user_action();
  508. if(source_buffer.has_selection) {
  509. TextIter iter_end;
  510. source_buffer.get_selection_bounds(out iter, out iter_end);
  511. source_buffer.delete(ref iter, ref iter_end);
  512. }
  513. else {
  514. source_buffer.get_iter_at_offset(out iter, source_buffer.cursor_position);
  515. }
  516. var link = @"![$label]($url)";
  517. source_buffer.insert(ref iter, link, link.length);
  518. source_buffer.end_user_action();
  519. }
  520. private void toggle_preview() {
  521. if(preview_button.active) {
  522. initialise_preview();
  523. update_preview(false);
  524. }
  525. else {
  526. destruct_preview();
  527. }
  528. }
  529. }
  530. public class AddLinkWindow : Adw.Window {
  531. public signal void insert_link(string url, string? text);
  532. private EntryRow url_entry;
  533. private EntryRow text_entry;
  534. public AddLinkWindow(string? text) {
  535. title = "Insert link";
  536. var box = new Box(Orientation.VERTICAL, 18);
  537. var header = new Adw.HeaderBar();
  538. header.show_end_title_buttons = false;
  539. box.append(header);
  540. var insert_button = new Button.with_label("Insert");
  541. var cancel_button = new Button.with_label("Cancel");
  542. insert_button.add_css_class("suggested-action");
  543. insert_button.clicked.connect(() => insert());
  544. cancel_button.clicked.connect(() => close());
  545. header.pack_start(cancel_button);
  546. header.pack_end(insert_button);
  547. var group = new PreferencesGroup();
  548. group.width_request = 400;
  549. group.title = "Link properties";
  550. group.margin_bottom = 18;
  551. group.margin_end = 18;
  552. group.margin_start = 18;
  553. url_entry = new EntryRow ();
  554. url_entry.title = "URI";
  555. group.add(url_entry);
  556. text_entry = new EntryRow ();
  557. text_entry.title = "Label (optional)";
  558. group.add(text_entry);
  559. if(text != null) {
  560. try {
  561. Uri.is_valid(text, UriFlags.NONE);
  562. url_entry.text = text;
  563. }
  564. catch (Error e) {
  565. text_entry.text = text;
  566. }
  567. }
  568. box.append(group);
  569. content = box;
  570. }
  571. private void insert() {
  572. string text = null;
  573. if(text_entry.text.chomp().chug() != "" || text_entry.text == url_entry.text) {
  574. text = text_entry.text;
  575. }
  576. insert_link(url_entry.text, text);
  577. close();
  578. }
  579. }
  580. public class AddImageWindow : Adw.Window {
  581. public signal void insert_image(string url, string text);
  582. private FileChooserRow file_entry;
  583. private EntryRow text_entry;
  584. public AddImageWindow(string? text, ViewerWindow win) {
  585. title = "Insert link";
  586. var box = new Box(Orientation.VERTICAL, 18);
  587. var header = new Adw.HeaderBar();
  588. header.show_end_title_buttons = false;
  589. box.append(header);
  590. var insert_button = new Button.with_label("Insert");
  591. var cancel_button = new Button.with_label("Cancel");
  592. insert_button.add_css_class("suggested-action");
  593. insert_button.clicked.connect(() => insert());
  594. cancel_button.clicked.connect(() => close());
  595. header.pack_start(cancel_button);
  596. header.pack_end(insert_button);
  597. var group = new PreferencesGroup();
  598. group.width_request = 400;
  599. group.title = "Link properties";
  600. group.margin_bottom = 18;
  601. group.margin_end = 18;
  602. group.margin_start = 18;
  603. file_entry = new FileChooserRow(win);
  604. file_entry.title = "Image file";
  605. file_entry.set_assets(win.publication.assets.where(a => a.mimetype.has_prefix ("image/")));
  606. group.add(file_entry);
  607. text_entry = new EntryRow ();
  608. text_entry.title = "Image description";
  609. group.add(text_entry);
  610. if(text != null) {
  611. text_entry.text = text;
  612. }
  613. box.append(group);
  614. content = box;
  615. }
  616. private void insert() {
  617. insert_image(file_entry.selected_asset, text_entry.text);
  618. close();
  619. }
  620. }
  621. }