removed 'never-worked' blogger shit
[k8lowj.git] / src / jamdoc.c
blob709487b513c06c4c5a7562d8be80c67843faed3f
1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
5 */
7 #include "glib-all.h"
9 #include <string.h>
11 #include "draftstore.h"
12 #include "jamdoc.h"
13 #include "account.h"
14 #include "conf.h"
15 #include "util.h"
17 #ifdef HAVE_GTK
18 #include "get_cmd_out.h"
19 #include "smartquotes.h"
20 #endif
22 #undef USE_STRUCTUREDTEXT
24 enum {
25 ENTRY_CHANGED,
26 UPDATE_DOC,
27 LAST_SIGNAL
30 enum {
31 PROP_0,
32 PROP_DIRTY,
33 PROP_USEJOURNAL,
34 PROP_ACCOUNT
37 struct _JamDoc {
38 GObject parent;
40 /* for drafts, the current entry is tracked through entry->itemid.
41 * for files, we need to keep the filename around. */
42 char *filename;
43 /* we only save as xml, so we need to remember whether they opened
44 * a non-xml file. */
45 gboolean filename_not_xml;
46 /* the convention is:
47 * if there's a filename, we're editing a file.
48 * if itemid is less than zero, we're editing a draft.
49 * otherwise, it's an untitled/unattached doc.
52 /* if autosave is on, each document gets its own autosave file.
53 * this is not related to the drafts mentioned above. */
54 char *autosave_filename;
56 gboolean dirty;
58 LJEntry *entry;
59 #ifdef HAVE_GTK
60 GtkTextBuffer *buffer;
61 #endif /* HAVE_GTK */
62 guint buffer_signal;
63 gchar *usejournal;
65 #ifdef USE_STRUCTUREDTEXT
66 TextStyle textstyle;
67 #endif
68 JamAccount *account;
70 gchar *url; /* entry URL; valid only after posting */
73 struct _JamDocClass {
74 GObjectClass parent_class;
75 void (*entry_changed)(JamDoc *doc);
76 void (*update_doc)(JamDoc *doc);
79 static void jam_doc_update_doc(JamDoc *doc);
80 static void jam_doc_set_property(GObject *object, guint prop_id,
81 const GValue *value, GParamSpec *pspec);
82 static void jam_doc_get_property(GObject *object, guint prop_id,
83 GValue *value, GParamSpec *pspec);
85 static guint signals[LAST_SIGNAL] = { 0 };
87 static void
88 jam_doc_class_init(gpointer klass, gpointer class_data) {
89 GObjectClass *gclass = G_OBJECT_CLASS(klass);
91 gclass->set_property = jam_doc_set_property;
92 gclass->get_property = jam_doc_get_property;
94 signals[ENTRY_CHANGED] = g_signal_new("entry_changed",
95 LOGJAM_TYPE_DOC,
96 G_SIGNAL_RUN_LAST,
97 G_STRUCT_OFFSET(JamDocClass, entry_changed),
98 NULL, NULL,
99 g_cclosure_marshal_VOID__VOID,
100 G_TYPE_NONE, 0);
102 signals[UPDATE_DOC] = g_signal_new("update_doc",
103 LOGJAM_TYPE_DOC,
104 G_SIGNAL_RUN_LAST,
105 G_STRUCT_OFFSET(JamDocClass, update_doc),
106 NULL, NULL,
107 g_cclosure_marshal_VOID__VOID,
108 G_TYPE_NONE, 0);
110 g_object_class_install_property(gclass, PROP_DIRTY,
111 g_param_spec_boolean("dirty",
112 "Is dirty",
113 "If TRUE, the document has changed since it was last saved.",
114 FALSE, G_PARAM_READWRITE));
116 g_object_class_install_property(gclass, PROP_USEJOURNAL,
117 g_param_spec_string("usejournal",
118 "use journal",
119 "The current journal that the account is working with.",
120 NULL, G_PARAM_READWRITE));
122 g_object_class_install_property(gclass, PROP_ACCOUNT,
123 g_param_spec_pointer("account",
124 "account",
125 "The current account that the user is working with.",
126 G_PARAM_READWRITE));
129 #ifdef HAVE_GTK
130 static void
131 buffer_changed_cb(GtkTextBuffer *buf, JamDoc *doc) {
132 if (gtk_text_buffer_get_modified(buf))
133 jam_doc_set_dirty(doc, TRUE);
136 GtkTextBuffer*
137 jam_doc_get_text_buffer(JamDoc *doc) {
138 return doc->buffer;
140 #endif /* HAVE_GTK */
142 static void
143 jam_doc_init(GTypeInstance *inst, gpointer g_class) {
144 JamDoc *doc = LOGJAM_DOC(inst);
145 doc->entry = lj_entry_new();
146 doc->entry->security = conf.defaultsecurity;
147 doc->dirty = FALSE;
148 #ifdef HAVE_GTK
149 doc->buffer = gtk_text_buffer_new(NULL);
150 doc->buffer_signal = g_signal_connect(G_OBJECT(doc->buffer),
151 "modified-changed",
152 G_CALLBACK(buffer_changed_cb), doc);
153 gtk_text_buffer_set_modified(doc->buffer, FALSE);
154 if (conf.options.smartquotes)
155 smartquotes_attach(doc->buffer, conf.options.smartquotes_russian);
156 #endif /* HAVE_GTK */
157 doc->url = NULL;
160 GType
161 jam_doc_get_type() {
162 static GType doc_type = 0;
163 if (!doc_type) {
164 static const GTypeInfo doc_info = {
165 sizeof(JamDocClass),
166 NULL,
167 NULL,
168 jam_doc_class_init,
169 NULL,
170 NULL,
171 sizeof(JamDoc),
173 jam_doc_init
175 doc_type = g_type_register_static(G_TYPE_OBJECT, "JamDoc",
176 &doc_info, 0);
178 return doc_type;
181 static void
182 jam_doc_set_property(GObject *object, guint prop_id,
183 const GValue *value, GParamSpec *pspec)
185 /* unimplemented. */
187 static void
188 jam_doc_get_property(GObject *object, guint prop_id,
189 GValue *value, GParamSpec *pspec)
191 /* unimplemented. */
194 JamDoc*
195 jam_doc_new(void) {
196 return LOGJAM_DOC(g_object_new(jam_doc_get_type(), NULL));
199 void
200 jam_doc_set_dirty(JamDoc *doc, gboolean dirty) {
201 if (doc->dirty == dirty)
202 return;
203 doc->dirty = dirty;
204 #ifdef HAVE_GTK
205 if (doc->dirty == FALSE)
206 gtk_text_buffer_set_modified(doc->buffer, FALSE);
207 #endif /* HAVE_GTK */
208 g_object_notify(G_OBJECT(doc), "dirty");
211 gboolean
212 jam_doc_get_dirty(JamDoc *doc) {
213 return doc->dirty;
216 const char *
217 jam_doc_get_subject(JamDoc *doc) {
218 return doc->entry->subject;
220 void
221 jam_doc_set_subject(JamDoc *doc, const char *subject) {
222 string_replace(&doc->entry->subject, subject ? g_strdup(subject) : NULL);
224 const char *
225 jam_doc_get_mood(JamDoc *doc) {
226 return doc->entry->mood;
228 void
229 jam_doc_set_mood(JamDoc *doc, const char *mood) {
230 string_replace(&doc->entry->mood, mood ? g_strdup(mood) : NULL);
232 void
233 jam_doc_set_moodid(JamDoc *doc, int moodid) {
234 doc->entry->moodid = moodid;
236 const char *
237 jam_doc_get_music(JamDoc *doc) {
238 return doc->entry->music;
240 void
241 jam_doc_set_music(JamDoc *doc, const char *music) {
242 string_replace(&doc->entry->music, music ? g_strdup(music) : NULL);
244 const char *
245 jam_doc_get_location(JamDoc *doc) {
246 return doc->entry->location;
248 void
249 jam_doc_set_location(JamDoc *doc, const char *location) {
250 string_replace(&doc->entry->location, location ? g_strdup(location) : NULL);
252 const char *
253 jam_doc_get_taglist(JamDoc *doc) {
254 return doc->entry->taglist;
256 void
257 jam_doc_set_taglist(JamDoc *doc, const char *taglist) {
258 string_replace(&doc->entry->taglist, taglist ? g_strdup(taglist) : NULL);
260 LJSecurity
261 jam_doc_get_security(JamDoc *doc) {
262 return doc->entry->security;
264 void
265 jam_doc_set_security(JamDoc *doc, LJSecurity *sec) {
266 doc->entry->security = *sec;
268 LJCommentsType
269 jam_doc_get_comments(JamDoc *doc) {
270 return doc->entry->comments;
272 void
273 jam_doc_set_comments(JamDoc *doc, LJCommentsType type) {
274 doc->entry->comments = type;
277 LJScreeningType
278 jam_doc_get_screening(JamDoc *doc) {
279 return doc->entry->screening;
281 void
282 jam_doc_set_screening(JamDoc *doc, LJScreeningType type) {
283 doc->entry->screening = type;
286 void
287 jam_doc_get_time(JamDoc *doc, struct tm *ptm) {
288 *ptm = doc->entry->time;
290 void
291 jam_doc_set_time(JamDoc *doc, const struct tm *ptm) {
292 if (ptm)
293 doc->entry->time = *ptm;
294 else
295 memset((void*)&doc->entry->time, 0, sizeof(struct tm));
298 gboolean
299 jam_doc_get_backdated(JamDoc *doc) {
300 return doc->entry->backdated;
302 void
303 jam_doc_set_backdated(JamDoc *doc, gboolean backdated) {
304 doc->entry->backdated = backdated;
307 gboolean
308 jam_doc_get_preformatted(JamDoc *doc) {
309 return doc->entry->preformatted;
311 void
312 jam_doc_set_preformatted(JamDoc *doc, gboolean preformatted) {
313 doc->entry->preformatted = preformatted;
316 void
317 jam_doc_set_event(JamDoc *doc, const char *event) {
318 g_free(doc->entry->event);
319 doc->entry->event = (event ? g_strdup(event) : NULL);
322 const char *
323 jam_doc_get_picture(JamDoc *doc) {
324 return doc->entry->pickeyword;
326 void
327 jam_doc_set_picture(JamDoc *doc, const char *picture) {
328 string_replace(&doc->entry->pickeyword, picture ? g_strdup(picture) : NULL);
330 void
331 jam_doc_set_pickeyword(JamDoc *doc, const char *keyword) {
332 jam_doc_set_picture(doc, keyword);
335 static void
336 set_entry(JamDoc *doc, LJEntry *entry) {
337 #ifdef HAVE_GTK
338 GtkTextIter start, end;
339 #endif /* HAVE_GTK */
341 lj_entry_free(doc->entry);
342 if (entry)
343 doc->entry = entry;
344 else
345 entry = doc->entry = lj_entry_new();
347 #ifdef HAVE_GTK
348 /* block the buffer signal so we don't rapidly flip to dirty and back. */
349 g_signal_handler_block(doc->buffer, doc->buffer_signal);
350 gtk_text_buffer_get_bounds(doc->buffer, &start, &end);
351 gtk_text_buffer_delete(doc->buffer, &start, &end);
352 if (entry->event)
353 gtk_text_buffer_insert(doc->buffer, &start, entry->event, -1);
354 g_signal_handler_unblock(doc->buffer, doc->buffer_signal);
355 #endif /* HAVE_GTK */
357 jam_doc_set_dirty(doc, FALSE);
358 #ifdef HAVE_GTK
359 /* since the buffer signal was blocked, we need to do this manually. */
360 gtk_text_buffer_set_modified(doc->buffer, FALSE);
361 #endif /* HAVE_GTK */
364 gboolean
365 jam_doc_load_file(JamDoc *doc, const char *filename,
366 LJEntryFileType type, GError **err) {
367 LJEntry *entry;
368 entry = lj_entry_new_from_filename(filename, type, &type, err);
369 if (!entry)
370 return FALSE;
371 entry->itemid = 0;
372 set_entry(doc, entry);
373 string_replace(&doc->filename, g_strdup(filename));
374 doc->filename_not_xml = (type != LJ_ENTRY_FILE_XML);
375 g_signal_emit_by_name(doc, "entry_changed");
376 return TRUE;
379 void
380 jam_doc_load_draft(JamDoc *doc, LJEntry *entry) {
381 set_entry(doc, lj_entry_copy(entry));
382 g_signal_emit_by_name(doc, "entry_changed");
385 void
386 jam_doc_load_entry(JamDoc *doc, LJEntry *entry) {
387 set_entry(doc, lj_entry_copy(entry));
388 g_signal_emit_by_name(doc, "entry_changed");
391 static gboolean
392 save_draft(LJEntry *entry, JamAccount *acc, GError **err) {
393 DraftStore *ds;
395 ds = draft_store_new(acc);
397 if (entry->itemid == 0)
398 entry->itemid = draft_store_find_itemid(ds);
400 if (!draft_store_put_entry(ds, entry, err)) {
401 draft_store_free(ds);
402 return FALSE;
405 draft_store_free(ds);
407 return TRUE;
410 gboolean
411 jam_doc_would_save_over_nonxml(JamDoc *doc) {
412 return (doc->filename && doc->filename_not_xml);
414 gboolean
415 jam_doc_has_save_target(JamDoc *doc) {
416 /* filename or a draft. */
417 return (doc->filename || doc->entry->itemid < 0);
420 gboolean
421 jam_doc_save(JamDoc *doc, JamAccount *acc, GError **err) {
422 jam_doc_update_doc(doc);
424 if (doc->filename) {
425 if (!lj_entry_to_xml_file(doc->entry, doc->filename, err))
426 return FALSE;
427 } else if (doc->entry->itemid < 0) {
428 if (!save_draft(doc->entry, acc, err))
429 return FALSE;
430 } else {
431 g_error("jam_doc_save: shouldn't be called without savetarget.\n");
432 return FALSE;
435 jam_doc_set_dirty(doc, FALSE);
437 return TRUE;
440 gboolean
441 jam_doc_save_as_file(JamDoc *doc, const char *filename, GError **err) {
442 LJEntry *entry;
444 entry = jam_doc_get_entry(doc);
446 /* always save to files without an itemid. */
447 entry->itemid = 0;
448 if (!lj_entry_to_xml_file(entry, filename, err)) {
449 lj_entry_free(entry);
450 return FALSE;
452 lj_entry_free(entry);
454 doc->entry->itemid = 0;
455 string_replace(&doc->filename, g_strdup(filename));
457 jam_doc_set_dirty(doc, FALSE);
459 return TRUE;
462 gboolean
463 jam_doc_save_as_draft(JamDoc *doc, const char *title, JamAccount *acc, GError **err) {
464 LJEntry *entry;
466 entry = jam_doc_get_entry(doc);
467 string_replace(&entry->subject, g_strdup(title));
468 entry->itemid = 0;
469 if (!save_draft(entry, acc, err)) {
470 lj_entry_free(entry);
471 return FALSE;
473 /* retrieve the new draft id. */
474 doc->entry->itemid = entry->itemid;
475 lj_entry_free(entry);
477 string_replace(&doc->filename, NULL);
479 jam_doc_set_dirty(doc, FALSE);
481 return TRUE;
484 char*
485 jam_doc_get_title(JamDoc *doc) {
486 if (doc->filename) {
487 const gchar *homedir = g_get_home_dir();
488 int homelen = (int)strlen(homedir);
489 if (strncmp(homedir, doc->filename, homelen) == 0)
490 return g_build_filename("~", doc->filename+homelen, NULL);
491 else
492 return g_strdup(doc->filename);
493 } else if (doc->entry->itemid < 0) {
494 return g_strdup_printf(_("Draft %d"), -doc->entry->itemid);
495 } else if (doc->entry->itemid > 0) {
496 return g_strdup_printf(_("Entry %d"), doc->entry->itemid);
497 } else {
498 return g_strdup(_("New Entry"));
502 char*
503 jam_doc_get_draftname(JamDoc *doc) {
504 jam_doc_update_doc(doc);
505 if (doc->entry->subject)
506 return g_strdup(doc->entry->subject);
507 return NULL;
510 static void
511 jam_doc_update_doc(JamDoc *doc) {
512 /* XXX this is sorta weird; we emit a signal to get our internal version
513 * of the entry in sync with the state of the widgets. this is important
514 * in decoupling the widgets from the document, but it feels a little
515 * backwards. */
516 g_signal_emit_by_name(doc, "update_doc");
519 LJEntry*
520 jam_doc_get_entry(JamDoc *doc) {
521 jam_doc_update_doc(doc);
522 return lj_entry_copy(doc->entry);
525 #ifdef HAVE_GTK
526 gboolean jam_doc_append_text (JamDoc *doc, const char *text, const char *encoding) {
527 if (text != NULL) {
528 GError *err = NULL;
529 GtkTextIter pos;
530 gtk_text_buffer_get_end_iter(doc->buffer, &pos);
531 gtk_text_buffer_insert(doc->buffer, &pos, text, -1);
532 if (strcmp(encoding, "UTF-8") == 0) {
533 const gchar *end;
534 if (!g_utf8_validate(text, strlen(text), &end)) return FALSE;
535 gtk_text_buffer_insert(doc->buffer, &pos, text, -1);
536 } else {
537 char *newtext;
538 newtext = g_convert(text, -1, "UTF-8", encoding, NULL, NULL, &err);
539 if (err) g_free(err);
540 if (!newtext) return FALSE;
541 gtk_text_buffer_insert(doc->buffer, &pos, newtext, -1);
542 g_free(newtext);
545 return TRUE;
548 gboolean
549 jam_doc_insert_file(JamDoc *doc, const char *filename, const char *encoding, GError **err) {
550 char *text;
551 gsize len;
553 if (!g_file_get_contents(filename, &text, &len, err))
554 return FALSE;
556 if (strcmp(encoding, "UTF-8") == 0) {
557 const gchar *end;
558 if (!g_utf8_validate(text, len, &end)) {
559 g_set_error(err, 0, 0,
560 _("Invalid UTF-8 starting at byte %d."), end-text);
561 g_free(text);
562 return FALSE;
564 gtk_text_buffer_insert_at_cursor(doc->buffer, text, -1);
565 } else {
566 char *newtext;
567 newtext = g_convert(text, -1, "UTF-8", encoding, NULL, NULL, err);
568 if (!newtext) {
569 g_free(text);
570 return FALSE;
572 gtk_text_buffer_insert_at_cursor(doc->buffer, newtext, -1);
573 g_free(newtext);
575 g_free(text);
576 return TRUE;
579 gboolean
580 jam_doc_insert_command_output(JamDoc *doc, const char *command,
581 const char *encoding, GError **err, GtkWindow *parent) {
582 /* FIXME: encoding is not currently used, instead we recode from
583 * the current locale. Does anybody knows a program which output
584 * is NOT in the current locale's encoding? If yes, let me know.
586 GString * output;
587 const gchar *end;
589 output = get_command_output(command, err, parent);
590 if (output == NULL)
591 return FALSE;
593 if (g_utf8_validate(output->str, output->len, &end)) {
594 gtk_text_buffer_insert_at_cursor(doc->buffer,
595 output->str, output->len);
596 } else {
597 gchar* newtext;
599 newtext = g_locale_to_utf8(output->str, output->len,
600 NULL, NULL, err);
601 if (!newtext) {
602 g_string_free(output, TRUE);
603 return FALSE;
605 gtk_text_buffer_insert_at_cursor(doc->buffer, newtext, -1);
606 g_free(newtext);
608 g_string_free(output, TRUE);
609 return TRUE;
611 #endif /* HAVE_GTK */
614 jam_doc_get_flags(JamDoc *doc) {
615 LJEntryType type = jam_doc_get_entry_type(doc);
616 int flags = 0;
618 switch (type) {
619 case ENTRY_DRAFT:
620 flags = LOGJAM_DOC_CAN_DELETE | LOGJAM_DOC_CAN_SUBMIT;
621 break;
622 case ENTRY_NEW:
623 flags = LOGJAM_DOC_CAN_SUBMIT;
624 break;
625 case ENTRY_SERVER:
626 flags = LOGJAM_DOC_CAN_DELETE | LOGJAM_DOC_CAN_SAVE;
627 break;
629 return flags;
632 LJEntryType
633 jam_doc_get_entry_type(JamDoc *doc) {
634 gint itemid = jam_doc_get_entry_itemid(doc);
635 return itemid < 0 ? ENTRY_DRAFT :
636 itemid == 0 ? ENTRY_NEW :
637 ENTRY_SERVER;
640 gint
641 jam_doc_get_entry_itemid(JamDoc *doc) {
642 return doc->entry->itemid;
645 static void
646 jam_doc_user_changed(JamDoc *doc) {
647 /* if they were editing an old entry or a draft,
648 * that itemid is no longer relevant for the current user. */
649 jam_doc_update_doc(doc);
650 if (doc->entry->itemid != 0) {
651 doc->entry->itemid = 0;
652 g_signal_emit_by_name(doc, "entry_changed");
656 gchar*
657 jam_doc_get_usejournal(JamDoc *doc) {
658 return doc->usejournal;
661 void
662 jam_doc_set_usejournal(JamDoc *doc, const gchar *usejournal) {
663 gchar *uj = usejournal ? g_strdup(usejournal) : NULL;
664 string_replace(&doc->usejournal, uj);
665 jam_doc_user_changed(doc);
666 g_object_notify(G_OBJECT(doc), "usejournal");
669 JamAccount*
670 jam_doc_get_account(JamDoc *doc) {
671 return doc->account;
674 void
675 jam_doc_set_account(JamDoc *doc, JamAccount *acc) {
676 g_object_ref(acc);
677 if (doc->account)
678 g_object_unref(doc->account);
679 doc->account = acc;
681 g_object_notify(G_OBJECT(doc), "account");
682 jam_doc_set_usejournal(doc, NULL);
683 /* set_usejournal calls this for us: jam_doc_user_changed(doc); */
686 #ifdef USE_STRUCTUREDTEXT
687 char* structuredtext_to_html(const char *src, GError **err);
688 char* html_to_structuredtext(const char *src, GError **err);
689 typedef char* (*TextStyleFunc)(const char *src, GError **err);
690 static TextStyleFunc to_html[] = {
691 NULL,
692 structuredtext_to_html
694 static TextStyleFunc from_html[] = {
695 NULL,
696 html_to_structuredtext
699 gboolean
700 jam_doc_change_textstyle(JamDoc *doc, TextStyle newstyle, GError **err) {
701 GtkTextBuffer *buffer = doc->buffer;
702 GtkTextIter start, end;
703 char *src;
704 char *html, *result;
706 gtk_text_buffer_get_bounds(buffer, &start, &end);
707 src = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
709 if (to_html[doc->jam_doc_get_textstyle(doc)])
710 html = to_html[doc->jam_doc_get_textstyle(doc)](src, err);
711 else
712 html = src;
714 if (html == NULL) {
715 g_free(src);
716 return FALSE;
719 if (from_html[newstyle])
720 result = from_html[newstyle](src, err);
721 else
722 result = html;
724 if (result == NULL) {
725 g_free(src);
726 if (html != src)
727 g_free(html);
728 return FALSE;
731 doc->textstyle = newstyle;
732 gtk_text_buffer_set_text(buffer, result, -1);
734 g_free(src);
735 if (src != html)
736 g_free(html);
737 if (html != result)
738 g_free(result);
739 return TRUE;
741 #endif /* USE_STRUCTUREDTEXT */
744 const char *jam_doc_get_url (JamDoc *doc) {
745 return doc->url;
749 void jam_doc_set_url (JamDoc *doc, const char *url) {
750 if (doc->url) g_free(doc->url);
751 if (url) doc->url = g_strdup(url); else doc->url = NULL;
755 void jam_doc_reset_url (JamDoc *doc) {
756 if (doc->url) g_free(doc->url);
757 doc->url = NULL;