Commit d6be4c4a authored by Darin Adler's avatar Darin Adler

New. (insert_node): New. (reparent_node): New. (update_node): Handle files

	* components/tree/nautilus-tree-model.h:
	* components/tree/nautilus-tree-model.c:
	(get_parent_node_from_file): New.
	(insert_node): New.
	(reparent_node): New.
	(update_node): Handle files that disappear or move.
	(process_file_change): Don't create nodes for files that are
	already gone by the time we get here.
	(done_loading_callback): Don't assert if the node for this file
	is gone by the time this shows up.
	(done_loading_idle_callback): New.
	(start_monitoring_directory): Do a done_loading callback for
	directories that are already fully loaded at the start. We have
	to do this at idle time, though, since the tree code can't handle
	all the new nodes showing up inside ref_node.
	(nautilus_tree_model_iter_n_children): Add; was missing.
	(destroy_unneeded_children): New.
	(destroy_unneeded_children_idle_callback): New.
	(last_child_unref): Destroy the children at idle time, not right
	away. I did this to try to make GtkTreeModelSort work, but it
	didn't fix the whole problem, so maybe take this out later.
	(nautilus_tree_model_unref_node): Tighten assert.
	(nautilus_tree_model_iter_get_file): New.
	(nautilus_tree_model_finalize): Cleanup for new idle functions.
	(nautilus_tree_model_tree_model_init):

	* components/tree/nautilus-tree-view.c: (load_expansion_state):
	New placeholder, not yet implemented.
	(path_to_file): New.
	(prepend_one_uri): New.
	(save_expansion_state): New.
	(save_expansion_state_idle_callback): New.
	(schedule_save_expansion_state_callback): New.
	(got_activation_uri_callback): New.
	(cancel_activation): New.
	(row_activated_callback): New.
	(create_tree): Put in first cut at using GtkTreeModelSort;
	couldn't get it working so it's ifdef'd out. Save list of expanded
	URIs next idle after a row is expanded or collapsed. Switch the
	view location when a row is activated.
	(tree_activate_callback): Put in a call to load_expansion_state,
	although it's not implemented yet.
	(nautilus_tree_view_finalize): Cleanup for new idle functions.
parent 2ea50bda
2002-02-07 Darin Adler <darin@bentspoon.com>
* components/tree/nautilus-tree-model.h:
* components/tree/nautilus-tree-model.c:
(get_parent_node_from_file): New.
(insert_node): New.
(reparent_node): New.
(update_node): Handle files that disappear or move.
(process_file_change): Don't create nodes for files that are
already gone by the time we get here.
(done_loading_callback): Don't assert if the node for this file
is gone by the time this shows up.
(done_loading_idle_callback): New.
(start_monitoring_directory): Do a done_loading callback for
directories that are already fully loaded at the start. We have
to do this at idle time, though, since the tree code can't handle
all the new nodes showing up inside ref_node.
(nautilus_tree_model_iter_n_children): Add; was missing.
(destroy_unneeded_children): New.
(destroy_unneeded_children_idle_callback): New.
(last_child_unref): Destroy the children at idle time, not right
away. I did this to try to make GtkTreeModelSort work, but it
didn't fix the whole problem, so maybe take this out later.
(nautilus_tree_model_unref_node): Tighten assert.
(nautilus_tree_model_iter_get_file): New.
(nautilus_tree_model_finalize): Cleanup for new idle functions.
(nautilus_tree_model_tree_model_init):
* components/tree/nautilus-tree-view.c: (load_expansion_state):
New placeholder, not yet implemented.
(path_to_file): New.
(prepend_one_uri): New.
(save_expansion_state): New.
(save_expansion_state_idle_callback): New.
(schedule_save_expansion_state_callback): New.
(got_activation_uri_callback): New.
(cancel_activation): New.
(row_activated_callback): New.
(create_tree): Put in first cut at using GtkTreeModelSort;
couldn't get it working so it's ifdef'd out. Save list of expanded
URIs next idle after a row is expanded or collapsed. Switch the
view location when a row is activated.
(tree_activate_callback): Put in a call to load_expansion_state,
although it's not implemented yet.
(nautilus_tree_view_finalize): Cleanup for new idle functions.
2002-02-08 Abel Cheung <maddog@linux.org.hk>
* configure.in: Added "zh_CN" to ALL_LINGUAS.
......
......@@ -29,13 +29,13 @@
#include <config.h>
#include "nautilus-tree-model.h"
#include <string.h>
#include <gtk/gtktreemodel.h>
#include <eel/eel-gtk-extensions.h>
#include <libgnome/gnome-i18n.h>
#include <libnautilus-private/nautilus-directory.h>
#include <libnautilus-private/nautilus-file.h>
#include <libnautilus-private/nautilus-file-attributes.h>
#include <libnautilus-private/nautilus-file.h>
#include <libnautilus-private/nautilus-icon-factory.h>
#include <string.h>
/* The user_data of the GtkTreeIter is the TreeNode pointer.
* It's NULL for the dummy node. If it's NULL, then user_data2
......@@ -75,8 +75,15 @@ struct NautilusTreeModelDetails {
TreeNode *root_node;
guint root_node_changed_id;
gboolean root_node_parented;
guint destroy_unneeded_children_idle_id;
};
typedef struct {
NautilusDirectory *directory;
NautilusTreeModel *model;
} DoneLoadingParameters;
static GObjectClass *parent_class;
static void destroy_node_without_reporting (NautilusTreeModel *model,
......@@ -304,6 +311,18 @@ get_node_from_file (NautilusTreeModel *model, NautilusFile *file)
return g_hash_table_lookup (model->details->file_to_node_map, file);
}
static TreeNode *
get_parent_node_from_file (NautilusTreeModel *model, NautilusFile *file)
{
NautilusFile *parent_file;
TreeNode *parent_node;
parent_file = nautilus_file_get_parent (file);
parent_node = get_node_from_file (model, parent_file);
nautilus_file_unref (parent_file);
return parent_node;
}
static TreeNode *
create_node_for_file (NautilusTreeModel *model, NautilusFile *file)
{
......@@ -500,6 +519,36 @@ update_node_without_reporting (NautilusTreeModel *model, TreeNode *node)
return changed;
}
static void
insert_node (NautilusTreeModel *model, TreeNode *parent, TreeNode *node)
{
tree_node_parent (node, parent);
update_node_without_reporting (model, node);
report_node_inserted (model, node);
}
static void
reparent_node (NautilusTreeModel *model, TreeNode *node)
{
GtkTreePath *path;
TreeNode *new_parent;
new_parent = get_parent_node_from_file (model, node->file);
if (new_parent == NULL || new_parent->directory == NULL) {
destroy_node (model, node);
return;
}
path = get_node_path (model, node);
tree_node_unparent (node);
gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
gtk_tree_path_free (path);
insert_node (model, new_parent, node);
}
static void
update_node (NautilusTreeModel *model, TreeNode *node)
{
......@@ -507,9 +556,16 @@ update_node (NautilusTreeModel *model, TreeNode *node)
gboolean had_directory, has_directory;
gboolean changed;
/* FIXME: Handle case where node is no longer its parent's
* child because it is gone, or moved.
*/
if (nautilus_file_is_gone (node->file)) {
destroy_node (model, node);
return;
}
if (node->parent != NULL && node->parent->directory != NULL
&& !nautilus_directory_contains_file (node->parent->directory, node->file)) {
reparent_node (model, node);
return;
}
had_dummy_child = tree_node_has_dummy_child (node);
had_directory = node->directory != NULL;
......@@ -539,8 +595,7 @@ static void
process_file_change (NautilusTreeModel *model,
NautilusFile *file)
{
TreeNode *node, *parent_node;
NautilusFile *parent;
TreeNode *node, *parent;
node = get_node_from_file (model, file);
if (node != NULL) {
......@@ -548,17 +603,16 @@ process_file_change (NautilusTreeModel *model,
return;
}
parent = nautilus_file_get_parent (file);
parent_node = get_node_from_file (model, parent);
nautilus_file_unref (parent);
if (parent_node == NULL) {
if (nautilus_file_is_gone (file)) {
return;
}
node = create_node_for_file (model, file);
tree_node_parent (node, parent_node);
update_node_without_reporting (model, node);
report_node_inserted (model, node);
parent = get_parent_node_from_file (model, file);
if (parent == NULL) {
return;
}
insert_node (model, parent, create_node_for_file (model, file));
}
static void
......@@ -586,9 +640,7 @@ done_loading_callback (NautilusDirectory *directory,
file = nautilus_directory_get_corresponding_file (directory);
node = get_node_from_file (model, file);
nautilus_file_unref (file);
g_assert (node != NULL);
if (node->done_loading) {
if (node == NULL || node->done_loading) {
return;
}
......@@ -614,11 +666,28 @@ get_tree_monitor_attributes (void)
return attrs;
}
static gboolean
done_loading_idle_callback (gpointer callback_data)
{
DoneLoadingParameters *p;
p = callback_data;
if (p->directory != NULL && p->model != NULL) {
done_loading_callback (p->directory, p->model);
}
eel_nullify_cancel (&p->model);
eel_nullify_cancel (&p->directory);
g_free (p);
return FALSE;
}
static void
start_monitoring_directory (NautilusTreeModel *model, TreeNode *node)
{
GList *attrs;
NautilusDirectory *directory;
GList *attrs;
DoneLoadingParameters *p;
if (node->done_loading_id != 0) {
return;
......@@ -645,6 +714,18 @@ start_monitoring_directory (NautilusTreeModel *model, TreeNode *node)
nautilus_directory_file_monitor_add (directory, model, TRUE, TRUE, attrs,
files_changed_callback, model);
g_list_free (attrs);
if (nautilus_directory_are_all_files_seen (directory)) {
/* Can't just remove the dummy node in here, so we do
* it at idle time.
*/
p = g_new (DoneLoadingParameters, 1);
p->directory = directory;
p->model = model;
eel_nullify_when_destroyed (&p->directory);
eel_nullify_when_destroyed (&p->model);
g_idle_add (done_loading_idle_callback, p);
}
}
static int
......@@ -868,6 +949,35 @@ nautilus_tree_model_iter_has_child (GtkTreeModel *model, GtkTreeIter *iter)
return node != NULL && node->directory != NULL;
}
static int
nautilus_tree_model_iter_n_children (GtkTreeModel *model, GtkTreeIter *iter)
{
NautilusTreeModel *tree_model;
TreeNode *parent, *node;
int n;
g_return_val_if_fail (NAUTILUS_IS_TREE_MODEL (model), FALSE);
g_return_val_if_fail (iter == NULL || iter_is_valid (NAUTILUS_TREE_MODEL (model), iter), FALSE);
tree_model = NAUTILUS_TREE_MODEL (model);
if (iter == NULL) {
return 1;
}
parent = iter->user_data;
if (parent == NULL) {
return 0;
}
n = tree_node_has_dummy_child (parent) ? 1 : 0;
for (node = parent->first_child; node != NULL; node = node->next) {
n++;
}
return n;
}
static gboolean
nautilus_tree_model_iter_nth_child (GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *parent_iter, int n)
{
......@@ -906,6 +1016,32 @@ nautilus_tree_model_iter_nth_child (GtkTreeModel *model, GtkTreeIter *iter, GtkT
return make_iter_for_node (node, iter, parent_iter->stamp);
}
static void
destroy_unneeded_children (NautilusTreeModel *model, TreeNode *node)
{
TreeNode *child;
if (node->child_ref_count == 0) {
stop_monitoring_directory (model, node);
destroy_children (model, node);
} else {
for (child = node->first_child; child != NULL; child = child->next) {
destroy_unneeded_children (model, child);
}
}
}
static gboolean
destroy_unneeded_children_idle_callback (gpointer callback_data)
{
NautilusTreeModel *model;
model = NAUTILUS_TREE_MODEL (callback_data);
model->details->destroy_unneeded_children_idle_id = 0;
destroy_unneeded_children (model, model->details->root_node);
return FALSE;
}
static void
first_child_ref (NautilusTreeModel *model, TreeNode *node)
{
......@@ -916,9 +1052,14 @@ first_child_ref (NautilusTreeModel *model, TreeNode *node)
static void
last_child_unref (NautilusTreeModel *model, TreeNode *node)
{
/* Destroying nodes in here causes trouble, at least when we
* use GtkTreeModelSort. I'm not sure if this is a bug or by
* design. For now, sidestep this issue by deferring until idle.
*/
g_assert (node->child_ref_count == 0);
stop_monitoring_directory (model, node);
destroy_children (model, node);
if (model->details->destroy_unneeded_children_idle_id == 0) {
g_idle_add (destroy_unneeded_children_idle_callback, model);
}
}
static void
......@@ -964,7 +1105,7 @@ nautilus_tree_model_unref_node (GtkTreeModel *model, GtkTreeIter *iter)
if (parent == NULL) {
g_assert (node == NAUTILUS_TREE_MODEL (model)->details->root_node);
} else {
g_assert (parent->child_ref_count >= 0);
g_assert (parent->child_ref_count > 0);
if (--parent->child_ref_count == 0) {
last_child_unref (NAUTILUS_TREE_MODEL (model), parent);
}
......@@ -1016,6 +1157,18 @@ nautilus_tree_model_new (const char *root_uri)
return model;
}
NautilusFile *
nautilus_tree_model_iter_get_file (NautilusTreeModel *model, GtkTreeIter *iter)
{
TreeNode *node;
g_return_val_if_fail (NAUTILUS_IS_TREE_MODEL (model), 0);
g_return_val_if_fail (iter_is_valid (NAUTILUS_TREE_MODEL (model), iter), 0);
node = iter->user_data;
return node == NULL ? NULL : nautilus_file_ref (node->file);
}
static void
nautilus_tree_model_init (NautilusTreeModel *model)
{
......@@ -1043,6 +1196,10 @@ nautilus_tree_model_finalize (GObject *object)
destroy_node_without_reporting (model, root);
}
if (model->details->destroy_unneeded_children_idle_id != 0) {
g_source_remove (model->details->destroy_unneeded_children_idle_id);
}
g_free (model->details);
G_OBJECT_CLASS (parent_class)->finalize (object);
......@@ -1067,8 +1224,9 @@ nautilus_tree_model_tree_model_init (GtkTreeModelIface *iface)
iface->iter_next = nautilus_tree_model_iter_next;
iface->iter_children = nautilus_tree_model_iter_children;
iface->iter_has_child = nautilus_tree_model_iter_has_child;
iface->iter_parent = nautilus_tree_model_iter_parent;
iface->iter_n_children = nautilus_tree_model_iter_n_children;
iface->iter_nth_child = nautilus_tree_model_iter_nth_child;
iface->iter_parent = nautilus_tree_model_iter_parent;
iface->ref_node = nautilus_tree_model_ref_node;
iface->unref_node = nautilus_tree_model_unref_node;
}
......
......@@ -28,6 +28,8 @@
#define NAUTILUS_TREE_MODEL_H
#include <glib-object.h>
#include <gtk/gtktreemodel.h>
#include <libnautilus-private/nautilus-file.h>
#define NAUTILUS_TYPE_TREE_MODEL (nautilus_tree_model_get_type ())
#define NAUTILUS_TREE_MODEL(obj) (GTK_CHECK_CAST ((obj), NAUTILUS_TYPE_TREE_MODEL, NautilusTreeModel))
......@@ -52,7 +54,9 @@ typedef struct {
GObjectClass parent_class;
} NautilusTreeModelClass;
GType nautilus_tree_model_get_type (void);
NautilusTreeModel *nautilus_tree_model_new (const char *root_uri);
GType nautilus_tree_model_get_type (void);
NautilusTreeModel *nautilus_tree_model_new (const char *root_uri);
NautilusFile * nautilus_tree_model_iter_get_file (NautilusTreeModel *model,
GtkTreeIter *iter);
#endif /* NAUTILUS_TREE_MODEL_H */
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
* Copyright (C) 2000, 2001 Eazel, Inc
......@@ -33,31 +33,196 @@
#include "nautilus-tree-view.h"
#include "nautilus-tree-model.h"
#include <eel/eel-preferences.h>
#include <eel/eel-string.h>
#include <gtk/gtkcellrendererpixbuf.h>
#include <gtk/gtkcellrenderertext.h>
#include <gtk/gtkscrolledwindow.h>
#include <gtk/gtktreemodelsort.h>
#include <gtk/gtktreeview.h>
#include <libnautilus-private/nautilus-file-attributes.h>
#define NAUTILUS_PREFERENCES_TREE_VIEW_EXPANSION_STATE "tree-sidebar-panel/expansion_state"
struct NautilusTreeViewDetails {
GtkWidget *scrolled_window;
GtkWidget *tree_view;
GtkTreeView *tree_widget;
#if SORT_MODEL_WORKS
GtkTreeModelSort *sort_model;
#else
NautilusTreeModel *sort_model;
#endif
NautilusTreeModel *child_model;
NautilusFile *activation_file;
guint save_expansion_state_idle_id;
};
typedef struct {
GList *uris;
NautilusTreeView *view;
} PrependURIParameters;
BONOBO_CLASS_BOILERPLATE (NautilusTreeView, nautilus_tree_view,
NautilusView, NAUTILUS_TYPE_VIEW)
static void
load_expansion_state (NautilusTreeView *view)
{
EelStringList *uris;
uris = eel_preferences_get_string_list (NAUTILUS_PREFERENCES_TREE_VIEW_EXPANSION_STATE);
/* eel_string_list_for_each (uris, expansion_state_load_callback, expansion_state); */
eel_string_list_free (uris);
/* FIXME: Need to expand nodes as they get loaded -- connect to the row_inserted signal on the model */
}
static NautilusFile *
path_to_file (NautilusTreeView *view, GtkTreePath *path)
{
GtkTreeIter iter;
if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (view->details->sort_model), &iter, path)) {
return NULL;
}
#if SORT_MODEL_WORKS
gtk_tree_model_sort_convert_iter_to_child_iter (view->details->sort_model, &iter, &iter);
#endif
return nautilus_tree_model_iter_get_file (view->details->child_model, &iter);
}
static void
prepend_one_uri (GtkTreeView *tree_view,
GtkTreePath *path,
gpointer callback_data)
{
PrependURIParameters *p;
NautilusFile *file;
p = callback_data;
file = path_to_file (p->view, path);
if (file == NULL) {
return;
}
p->uris = g_list_prepend (p->uris, nautilus_file_get_uri (file));
nautilus_file_unref (file);
}
static void
save_expansion_state (NautilusTreeView *view)
{
PrependURIParameters p;
EelStringList *uris;
p.uris = NULL;
p.view = view;
gtk_tree_view_map_expanded_rows (view->details->tree_widget, prepend_one_uri, &p);
p.uris = g_list_sort (p.uris, eel_strcmp_compare_func);
uris = eel_string_list_new_from_g_list (p.uris, TRUE);
g_list_free (p.uris);
eel_preferences_set_string_list (NAUTILUS_PREFERENCES_TREE_VIEW_EXPANSION_STATE, uris);
eel_string_list_free (uris);
}
static gboolean
save_expansion_state_idle_callback (gpointer callback_data)
{
NautilusTreeView *view;
view = NAUTILUS_TREE_VIEW (callback_data);
view->details->save_expansion_state_idle_id = 0;
save_expansion_state (view);
return FALSE;
}
static void
schedule_save_expansion_state_callback (NautilusTreeView *view)
{
g_assert (NAUTILUS_IS_TREE_VIEW (view));
if (view->details->save_expansion_state_idle_id == 0) {
view->details->save_expansion_state_idle_id =
g_idle_add (save_expansion_state_idle_callback, view);
}
}
static void
got_activation_uri_callback (NautilusFile *file, gpointer callback_data)
{
char *uri;
NautilusTreeView *view;
view = NAUTILUS_TREE_VIEW (callback_data);
g_assert (file == view->details->activation_file);
uri = nautilus_file_get_activation_uri (file);
if (uri != NULL
/* FIXME: reenable && !eel_uris_match_ignore_fragments (view->details->current_main_view_uri, uri) */
&& strncmp (uri, "command:", strlen ("command:")) != 0) {
nautilus_view_open_location_in_this_window (NAUTILUS_VIEW (view), uri);
}
g_free (uri);
/* FIXME: show_file (view, file); */
nautilus_file_unref (view->details->activation_file);
view->details->activation_file = NULL;
}
static void
cancel_activation (NautilusTreeView *view)
{
if (view->details->activation_file == NULL) {
return;
}
nautilus_file_cancel_call_when_ready
(view->details->activation_file,
got_activation_uri_callback, view);
nautilus_file_unref (view->details->activation_file);
view->details->activation_file = NULL;
}
static void
row_activated_callback (GtkTreeView *tree_widget,
GtkTreePath *path,
GtkTreeViewColumn *column,
NautilusTreeView *view)
{
GList *attrs;
cancel_activation (view);
view->details->activation_file = nautilus_file_ref (path_to_file (view, path));
attrs = g_list_prepend (NULL, NAUTILUS_FILE_ATTRIBUTE_ACTIVATION_URI);
nautilus_file_call_when_ready (view->details->activation_file, attrs,
got_activation_uri_callback, view);
g_list_free (attrs);
}
static void
create_tree (NautilusTreeView *view)
{
NautilusTreeModel *model;
GtkTreeViewColumn *column;
GtkCellRenderer *cell;
model = nautilus_tree_model_new ("file:///");
view->details->tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model));
g_object_unref (model);
view->details->child_model = nautilus_tree_model_new ("file:///");
#if SORT_MODEL_WORKS
view->details->sort_model = GTK_TREE_MODEL_SORT (gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (view->details->child_model)));
g_object_unref (view->details->child_model);
#else
view->details->sort_model = view->details->child_model;
#endif
view->details->tree_widget = GTK_TREE_VIEW (gtk_tree_view_new_with_model (GTK_TREE_MODEL (view->details->sort_model)));
g_object_unref (view->details->sort_model);
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (view->details->tree_view), FALSE);
#if SORT_MODEL_WORKS
/* gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (sort_model), func, data); */
#endif
gtk_tree_view_set_headers_visible (view->details->tree_widget, FALSE);
/* Create column */
column = gtk_tree_view_column_new ();
......@@ -74,12 +239,19 @@ create_tree (NautilusTreeView *view)
"text", NAUTILUS_TREE_MODEL_DISPLAY_NAME_COLUMN,
NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (view->details->tree_view), column);
gtk_tree_view_append_column (view->details->tree_widget, column);
gtk_widget_show (view->details->tree_view);
gtk_widget_show (GTK_WIDGET (view->details->tree_widget));
gtk_container_add (GTK_CONTAINER (view->details->scrolled_window),
view->details->tree_view);
GTK_WIDGET (view->details->tree_widget));
g_signal_connect_swapped (view->details->tree_widget, "row_expanded",
G_CALLBACK (schedule_save_expansion_state_callback), view);
g_signal_connect_swapped (view->details->tree_widget, "row_collapsed",
G_CALLBACK (schedule_save_expansion_state_callback), view);
g_signal_connect (view->details->tree_widget, "row_activated",
G_CALLBACK (row_activated_callback), view);
}
static void
......@@ -89,8 +261,9 @@ tree_activate_callback (BonoboControl *control, gboolean activating, gpointer us
view = NAUTILUS_TREE_VIEW (user_data);
if (activating && view->details->tree_view == NULL) {
if (activating && view->details->tree_widget == NULL) {
create_tree (view);
load_expansion_state (view);
}
}
......@@ -112,7 +285,7 @@ nautilus_tree_view_instance_init (NautilusTreeView *view)
control = bonobo_control_new (view->details->scrolled_window);
g_signal_connect_object (control, "activate",
G_CALLBACK (tree_activate_callback), view, 0);
nautilus_view_construct_from_bonobo_control (NAUTILUS_VIEW (view), control);
}
......@@ -123,6 +296,11 @@ nautilus_tree_view_finalize (GObject *object)
view = NAUTILUS_TREE_VIEW (object);
cancel_activation (view);
if (view->details->save_expansion_state_idle_id != 0) {
g_source_remove (view->details->save_expansion_state_idle_id);
}
g_free (view->details);
G_OBJECT_CLASS (parent_class)->finalize (object);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment