1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
11 #include "draftstore.h"
18 #include "get_cmd_out.h"
19 #include "smartquotes.h"
22 #undef USE_STRUCTUREDTEXT
40 /* for drafts, the current entry is tracked through entry->itemid.
41 * for files, we need to keep the filename around. */
43 /* we only save as xml, so we need to remember whether they opened
45 gboolean filename_not_xml
;
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
;
60 GtkTextBuffer
*buffer
;
65 #ifdef USE_STRUCTUREDTEXT
70 gchar
*url
; /* entry URL; valid only after posting */
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 };
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",
97 G_STRUCT_OFFSET(JamDocClass
, entry_changed
),
99 g_cclosure_marshal_VOID__VOID
,
102 signals
[UPDATE_DOC
] = g_signal_new("update_doc",
105 G_STRUCT_OFFSET(JamDocClass
, update_doc
),
107 g_cclosure_marshal_VOID__VOID
,
110 g_object_class_install_property(gclass
, PROP_DIRTY
,
111 g_param_spec_boolean("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",
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",
125 "The current account that the user is working with.",
131 buffer_changed_cb(GtkTextBuffer
*buf
, JamDoc
*doc
) {
132 if (gtk_text_buffer_get_modified(buf
))
133 jam_doc_set_dirty(doc
, TRUE
);
137 jam_doc_get_text_buffer(JamDoc
*doc
) {
140 #endif /* HAVE_GTK */
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
;
149 doc
->buffer
= gtk_text_buffer_new(NULL
);
150 doc
->buffer_signal
= g_signal_connect(G_OBJECT(doc
->buffer
),
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 */
162 static GType doc_type
= 0;
164 static const GTypeInfo doc_info
= {
175 doc_type
= g_type_register_static(G_TYPE_OBJECT
, "JamDoc",
182 jam_doc_set_property(GObject
*object
, guint prop_id
,
183 const GValue
*value
, GParamSpec
*pspec
)
188 jam_doc_get_property(GObject
*object
, guint prop_id
,
189 GValue
*value
, GParamSpec
*pspec
)
196 return LOGJAM_DOC(g_object_new(jam_doc_get_type(), NULL
));
200 jam_doc_set_dirty(JamDoc
*doc
, gboolean dirty
) {
201 if (doc
->dirty
== dirty
)
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");
212 jam_doc_get_dirty(JamDoc
*doc
) {
217 jam_doc_get_subject(JamDoc
*doc
) {
218 return doc
->entry
->subject
;
221 jam_doc_set_subject(JamDoc
*doc
, const char *subject
) {
222 string_replace(&doc
->entry
->subject
, subject
? g_strdup(subject
) : NULL
);
225 jam_doc_get_mood(JamDoc
*doc
) {
226 return doc
->entry
->mood
;
229 jam_doc_set_mood(JamDoc
*doc
, const char *mood
) {
230 string_replace(&doc
->entry
->mood
, mood
? g_strdup(mood
) : NULL
);
233 jam_doc_set_moodid(JamDoc
*doc
, int moodid
) {
234 doc
->entry
->moodid
= moodid
;
237 jam_doc_get_music(JamDoc
*doc
) {
238 return doc
->entry
->music
;
241 jam_doc_set_music(JamDoc
*doc
, const char *music
) {
242 string_replace(&doc
->entry
->music
, music
? g_strdup(music
) : NULL
);
245 jam_doc_get_location(JamDoc
*doc
) {
246 return doc
->entry
->location
;
249 jam_doc_set_location(JamDoc
*doc
, const char *location
) {
250 string_replace(&doc
->entry
->location
, location
? g_strdup(location
) : NULL
);
253 jam_doc_get_taglist(JamDoc
*doc
) {
254 return doc
->entry
->taglist
;
257 jam_doc_set_taglist(JamDoc
*doc
, const char *taglist
) {
258 string_replace(&doc
->entry
->taglist
, taglist
? g_strdup(taglist
) : NULL
);
261 jam_doc_get_security(JamDoc
*doc
) {
262 return doc
->entry
->security
;
265 jam_doc_set_security(JamDoc
*doc
, LJSecurity
*sec
) {
266 doc
->entry
->security
= *sec
;
269 jam_doc_get_comments(JamDoc
*doc
) {
270 return doc
->entry
->comments
;
273 jam_doc_set_comments(JamDoc
*doc
, LJCommentsType type
) {
274 doc
->entry
->comments
= type
;
278 jam_doc_get_screening(JamDoc
*doc
) {
279 return doc
->entry
->screening
;
282 jam_doc_set_screening(JamDoc
*doc
, LJScreeningType type
) {
283 doc
->entry
->screening
= type
;
287 jam_doc_get_time(JamDoc
*doc
, struct tm
*ptm
) {
288 *ptm
= doc
->entry
->time
;
291 jam_doc_set_time(JamDoc
*doc
, const struct tm
*ptm
) {
293 doc
->entry
->time
= *ptm
;
295 memset((void*)&doc
->entry
->time
, 0, sizeof(struct tm
));
299 jam_doc_get_backdated(JamDoc
*doc
) {
300 return doc
->entry
->backdated
;
303 jam_doc_set_backdated(JamDoc
*doc
, gboolean backdated
) {
304 doc
->entry
->backdated
= backdated
;
308 jam_doc_get_preformatted(JamDoc
*doc
) {
309 return doc
->entry
->preformatted
;
312 jam_doc_set_preformatted(JamDoc
*doc
, gboolean preformatted
) {
313 doc
->entry
->preformatted
= preformatted
;
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
);
323 jam_doc_get_picture(JamDoc
*doc
) {
324 return doc
->entry
->pickeyword
;
327 jam_doc_set_picture(JamDoc
*doc
, const char *picture
) {
328 string_replace(&doc
->entry
->pickeyword
, picture
? g_strdup(picture
) : NULL
);
331 jam_doc_set_pickeyword(JamDoc
*doc
, const char *keyword
) {
332 jam_doc_set_picture(doc
, keyword
);
336 set_entry(JamDoc
*doc
, LJEntry
*entry
) {
338 GtkTextIter start
, end
;
339 #endif /* HAVE_GTK */
341 lj_entry_free(doc
->entry
);
345 entry
= doc
->entry
= lj_entry_new();
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
);
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
);
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 */
365 jam_doc_load_file(JamDoc
*doc
, const char *filename
,
366 LJEntryFileType type
, GError
**err
) {
368 entry
= lj_entry_new_from_filename(filename
, type
, &type
, err
);
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");
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");
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");
392 save_draft(LJEntry
*entry
, JamAccount
*acc
, GError
**err
) {
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
);
405 draft_store_free(ds
);
411 jam_doc_would_save_over_nonxml(JamDoc
*doc
) {
412 return (doc
->filename
&& doc
->filename_not_xml
);
415 jam_doc_has_save_target(JamDoc
*doc
) {
416 /* filename or a draft. */
417 return (doc
->filename
|| doc
->entry
->itemid
< 0);
421 jam_doc_save(JamDoc
*doc
, JamAccount
*acc
, GError
**err
) {
422 jam_doc_update_doc(doc
);
425 if (!lj_entry_to_xml_file(doc
->entry
, doc
->filename
, err
))
427 } else if (doc
->entry
->itemid
< 0) {
428 if (!save_draft(doc
->entry
, acc
, err
))
431 g_error("jam_doc_save: shouldn't be called without savetarget.\n");
435 jam_doc_set_dirty(doc
, FALSE
);
441 jam_doc_save_as_file(JamDoc
*doc
, const char *filename
, GError
**err
) {
444 entry
= jam_doc_get_entry(doc
);
446 /* always save to files without an itemid. */
448 if (!lj_entry_to_xml_file(entry
, filename
, err
)) {
449 lj_entry_free(entry
);
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
);
463 jam_doc_save_as_draft(JamDoc
*doc
, const char *title
, JamAccount
*acc
, GError
**err
) {
466 entry
= jam_doc_get_entry(doc
);
467 string_replace(&entry
->subject
, g_strdup(title
));
469 if (!save_draft(entry
, acc
, err
)) {
470 lj_entry_free(entry
);
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
);
485 jam_doc_get_title(JamDoc
*doc
) {
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
);
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
);
498 return g_strdup(_("New Entry"));
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
);
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
516 g_signal_emit_by_name(doc
, "update_doc");
520 jam_doc_get_entry(JamDoc
*doc
) {
521 jam_doc_update_doc(doc
);
522 return lj_entry_copy(doc
->entry
);
526 gboolean
jam_doc_append_text (JamDoc
*doc
, const char *text
, const char *encoding
) {
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) {
534 if (!g_utf8_validate(text
, strlen(text
), &end
)) return FALSE
;
535 gtk_text_buffer_insert(doc
->buffer
, &pos
, text
, -1);
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);
549 jam_doc_insert_file(JamDoc
*doc
, const char *filename
, const char *encoding
, GError
**err
) {
553 if (!g_file_get_contents(filename
, &text
, &len
, err
))
556 if (strcmp(encoding
, "UTF-8") == 0) {
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
);
564 gtk_text_buffer_insert_at_cursor(doc
->buffer
, text
, -1);
567 newtext
= g_convert(text
, -1, "UTF-8", encoding
, NULL
, NULL
, err
);
572 gtk_text_buffer_insert_at_cursor(doc
->buffer
, newtext
, -1);
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.
589 output
= get_command_output(command
, err
, parent
);
593 if (g_utf8_validate(output
->str
, output
->len
, &end
)) {
594 gtk_text_buffer_insert_at_cursor(doc
->buffer
,
595 output
->str
, output
->len
);
599 newtext
= g_locale_to_utf8(output
->str
, output
->len
,
602 g_string_free(output
, TRUE
);
605 gtk_text_buffer_insert_at_cursor(doc
->buffer
, newtext
, -1);
608 g_string_free(output
, TRUE
);
611 #endif /* HAVE_GTK */
614 jam_doc_get_flags(JamDoc
*doc
) {
615 LJEntryType type
= jam_doc_get_entry_type(doc
);
620 flags
= LOGJAM_DOC_CAN_DELETE
| LOGJAM_DOC_CAN_SUBMIT
;
623 flags
= LOGJAM_DOC_CAN_SUBMIT
;
626 flags
= LOGJAM_DOC_CAN_DELETE
| LOGJAM_DOC_CAN_SAVE
;
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
:
641 jam_doc_get_entry_itemid(JamDoc
*doc
) {
642 return doc
->entry
->itemid
;
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");
657 jam_doc_get_usejournal(JamDoc
*doc
) {
658 return doc
->usejournal
;
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");
670 jam_doc_get_account(JamDoc
*doc
) {
675 jam_doc_set_account(JamDoc
*doc
, JamAccount
*acc
) {
678 g_object_unref(doc
->account
);
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
[] = {
692 structuredtext_to_html
694 static TextStyleFunc from_html
[] = {
696 html_to_structuredtext
700 jam_doc_change_textstyle(JamDoc
*doc
, TextStyle newstyle
, GError
**err
) {
701 GtkTextBuffer
*buffer
= doc
->buffer
;
702 GtkTextIter start
, end
;
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
);
719 if (from_html
[newstyle
])
720 result
= from_html
[newstyle
](src
, err
);
724 if (result
== NULL
) {
731 doc
->textstyle
= newstyle
;
732 gtk_text_buffer_set_text(buffer
, result
, -1);
741 #endif /* USE_STRUCTUREDTEXT */
744 const char *jam_doc_get_url (JamDoc
*doc
) {
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
);