tornavis/source/blender/editors/space_text/text_ops.cc

4311 lines
107 KiB
C++

/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sptext
*/
#include <cerrno>
#include <cstring>
#include <sstream>
#include "MEM_guardedalloc.h"
#include "DNA_text_types.h"
#include "BLI_blenlib.h"
#include "BLI_math_base.h"
#include "BLI_math_vector.h"
#include "BLI_string_cursor_utf8.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.h"
#include "PIL_time.h"
#include "BKE_context.hh"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_report.h"
#include "BKE_text.h"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curve.hh"
#include "ED_screen.hh"
#include "ED_text.hh"
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#ifdef WITH_PYTHON
# include "BPY_extern.h"
# include "BPY_extern_run.h"
#endif
#include "text_format.hh"
#include "text_intern.hh"
static void txt_screen_clamp(SpaceText *st, ARegion *region);
/* -------------------------------------------------------------------- */
/** \name Utilities
* \{ */
/**
* Tests if the given character represents a start of a new line or the
* indentation part of a line.
* \param c: The current character.
* \param r_last_state: A pointer to a flag representing the last state. The
* flag may be modified.
*/
static void test_line_start(char c, bool *r_last_state)
{
if (c == '\n') {
*r_last_state = true;
}
else if (!ELEM(c, '\t', ' ')) {
*r_last_state = false;
}
}
/**
* This function receives a character and returns its closing pair if it exists.
* \param character: Character to find the closing pair.
* \return The closing pair of the character if it exists.
*/
static char text_closing_character_pair_get(const char character)
{
switch (character) {
case '(':
return ')';
case '[':
return ']';
case '{':
return '}';
case '"':
return '"';
case '\'':
return '\'';
default:
return 0;
}
}
/**
* This function receives a range and returns true if the range is blank.
* \param line1: The first TextLine argument.
* \param line1_char: The character number of line1.
* \param line2: The second TextLine argument.
* \param line2_char: The character number of line2.
* \return True if the span is blank.
*/
static bool text_span_is_blank(TextLine *line1,
const int line1_char,
TextLine *line2,
const int line2_char)
{
const TextLine *start_line;
const TextLine *end_line;
int start_char;
int end_char;
/* Get the start and end lines. */
if (txt_get_span(line1, line2) > 0 || (line1 == line2 && line1_char <= line2_char)) {
start_line = line1;
end_line = line2;
start_char = line1_char;
end_char = line2_char;
}
else {
start_line = line2;
end_line = line1;
start_char = line2_char;
end_char = line1_char;
}
for (const TextLine *line = start_line; line != end_line->next; line = line->next) {
const int start = (line == start_line) ? start_char : 0;
const int end = (line == end_line) ? end_char : line->len;
for (int i = start; i < end; i++) {
if (!ELEM(line->line[i], ' ', '\t', '\n')) {
return false;
}
}
}
return true;
}
/**
* This function converts the indentation tabs from a buffer to spaces.
* \param in_buf: A pointer to a cstring.
* \param tab_size: The size, in spaces, of the tab character.
* \param r_out_buf_len: The #strlen of the returned buffer.
*/
static char *buf_tabs_to_spaces(const char *in_buf, const int tab_size, int *r_out_buf_len)
{
/* Get the number of tab characters in buffer. */
bool line_start = true;
int num_tabs = 0;
for (int in_offset = 0; in_buf[in_offset]; in_offset++) {
/* Verify if is an indentation whitespace character. */
test_line_start(in_buf[in_offset], &line_start);
if (in_buf[in_offset] == '\t' && line_start) {
num_tabs++;
}
}
/* Allocate output before with extra space for expanded tabs. */
const int out_size = strlen(in_buf) + num_tabs * (tab_size - 1) + 1;
char *out_buf = static_cast<char *>(MEM_mallocN(out_size * sizeof(char), __func__));
/* Fill output buffer. */
int spaces_until_tab = 0;
int out_offset = 0;
line_start = true;
for (int in_offset = 0; in_buf[in_offset]; in_offset++) {
/* Verify if is an indentation whitespace character. */
test_line_start(in_buf[in_offset], &line_start);
if (in_buf[in_offset] == '\t' && line_start) {
/* Calculate tab size so it fills until next indentation. */
int num_spaces = tab_size - (spaces_until_tab % tab_size);
spaces_until_tab = 0;
/* Write to buffer. */
memset(&out_buf[out_offset], ' ', num_spaces);
out_offset += num_spaces;
}
else {
if (in_buf[in_offset] == ' ') {
spaces_until_tab++;
}
else if (in_buf[in_offset] == '\n') {
spaces_until_tab = 0;
}
out_buf[out_offset++] = in_buf[in_offset];
}
}
out_buf[out_offset] = '\0';
*r_out_buf_len = out_offset;
return out_buf;
}
BLI_INLINE int text_pixel_x_to_column(SpaceText *st, const int x)
{
/* Add half the char width so mouse cursor selection is in between letters. */
return (x + (st->runtime.cwidth_px / 2)) / st->runtime.cwidth_px;
}
static void text_select_update_primary_clipboard(const Text *text)
{
if ((WM_capabilities_flag() & WM_CAPABILITY_PRIMARY_CLIPBOARD) == 0) {
return;
}
if (!txt_has_sel(text)) {
return;
}
char *buf = txt_sel_to_buf(text, nullptr);
if (buf == nullptr) {
return;
}
WM_clipboard_text_set(buf, true);
MEM_freeN(buf);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operator Poll
* \{ */
static bool text_new_poll(bContext * /*C*/)
{
return true;
}
static bool text_data_poll(bContext *C)
{
Text *text = CTX_data_edit_text(C);
if (!text) {
return false;
}
return true;
}
static bool text_edit_poll(bContext *C)
{
Text *text = CTX_data_edit_text(C);
if (!text) {
return false;
}
if (!BKE_id_is_editable(CTX_data_main(C), &text->id)) {
// BKE_report(op->reports, RPT_ERROR, "Cannot edit external library data");
return false;
}
return true;
}
bool text_space_edit_poll(bContext *C)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
if (!st || !text) {
return false;
}
if (!BKE_id_is_editable(CTX_data_main(C), &text->id)) {
// BKE_report(op->reports, RPT_ERROR, "Cannot edit external library data");
return false;
}
return true;
}
static bool text_region_edit_poll(bContext *C)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
ARegion *region = CTX_wm_region(C);
if (!st || !text) {
return false;
}
if (!region || region->regiontype != RGN_TYPE_WINDOW) {
return false;
}
if (!BKE_id_is_editable(CTX_data_main(C), &text->id)) {
// BKE_report(op->reports, RPT_ERROR, "Cannot edit external library data");
return false;
}
return true;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Updates
* \{ */
void text_update_line_edited(TextLine *line)
{
if (!line) {
return;
}
/* we just free format here, and let it rebuild during draw */
MEM_SAFE_FREE(line->format);
}
void text_update_edited(Text *text)
{
LISTBASE_FOREACH (TextLine *, line, &text->lines) {
text_update_line_edited(line);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name New Operator
* \{ */
static int text_new_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Main *bmain = CTX_data_main(C);
Text *text;
PointerRNA ptr;
PropertyRNA *prop;
text = BKE_text_add(bmain, DATA_("Text"));
/* hook into UI */
UI_context_active_but_prop_get_templateID(C, &ptr, &prop);
if (prop) {
PointerRNA idptr = RNA_id_pointer_create(&text->id);
RNA_property_pointer_set(&ptr, prop, idptr, nullptr);
RNA_property_update(C, &ptr, prop);
}
else if (st) {
st->text = text;
st->left = 0;
st->top = 0;
st->runtime.scroll_ofs_px[0] = 0;
st->runtime.scroll_ofs_px[1] = 0;
text_drawcache_tag_update(st, true);
}
WM_event_add_notifier(C, NC_TEXT | NA_ADDED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_new(wmOperatorType *ot)
{
/* identifiers */
ot->name = "New Text";
ot->idname = "TEXT_OT_new";
ot->description = "Create a new text data-block";
/* api callbacks */
ot->exec = text_new_exec;
ot->poll = text_new_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Open Operator
* \{ */
static void text_open_init(bContext *C, wmOperator *op)
{
PropertyPointerRNA *pprop = static_cast<PropertyPointerRNA *>(
MEM_callocN(sizeof(PropertyPointerRNA), "OpenPropertyPointerRNA"));
op->customdata = pprop;
UI_context_active_but_prop_get_templateID(C, &pprop->ptr, &pprop->prop);
}
static void text_open_cancel(bContext * /*C*/, wmOperator *op)
{
MEM_freeN(op->customdata);
}
static int text_open_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Main *bmain = CTX_data_main(C);
Text *text;
PropertyPointerRNA *pprop;
char filepath[FILE_MAX];
const bool internal = RNA_boolean_get(op->ptr, "internal");
RNA_string_get(op->ptr, "filepath", filepath);
text = BKE_text_load_ex(bmain, filepath, BKE_main_blendfile_path(bmain), internal);
if (!text) {
if (op->customdata) {
MEM_freeN(op->customdata);
}
return OPERATOR_CANCELLED;
}
if (!op->customdata) {
text_open_init(C, op);
}
/* hook into UI */
pprop = static_cast<PropertyPointerRNA *>(op->customdata);
if (pprop->prop) {
PointerRNA idptr = RNA_id_pointer_create(&text->id);
RNA_property_pointer_set(&pprop->ptr, pprop->prop, idptr, nullptr);
RNA_property_update(C, &pprop->ptr, pprop->prop);
}
else if (st) {
st->text = text;
st->left = 0;
st->top = 0;
st->runtime.scroll_ofs_px[0] = 0;
st->runtime.scroll_ofs_px[1] = 0;
}
text_drawcache_tag_update(st, true);
WM_event_add_notifier(C, NC_TEXT | NA_ADDED, text);
MEM_freeN(op->customdata);
return OPERATOR_FINISHED;
}
static int text_open_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
Main *bmain = CTX_data_main(C);
Text *text = CTX_data_edit_text(C);
const char *path = (text && text->filepath) ? text->filepath : BKE_main_blendfile_path(bmain);
if (RNA_struct_property_is_set(op->ptr, "filepath")) {
return text_open_exec(C, op);
}
text_open_init(C, op);
RNA_string_set(op->ptr, "filepath", path);
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
void TEXT_OT_open(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Open Text";
ot->idname = "TEXT_OT_open";
ot->description = "Open a new text data-block";
/* api callbacks */
ot->exec = text_open_exec;
ot->invoke = text_open_invoke;
ot->cancel = text_open_cancel;
ot->poll = text_new_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_TEXT | FILE_TYPE_PYSCRIPT,
FILE_SPECIAL,
FILE_OPENFILE,
WM_FILESEL_FILEPATH,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT); /* TODO: relative_path. */
RNA_def_boolean(
ot->srna, "internal", false, "Make Internal", "Make text file internal after loading");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Reload Operator
* \{ */
static int text_reload_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
ARegion *region = CTX_wm_region(C);
/* store view & cursor state */
const int orig_top = st->top;
const int orig_curl = BLI_findindex(&text->lines, text->curl);
const int orig_curc = text->curc;
/* Don't make this part of 'poll', since 'Alt-R' will type 'R',
* if poll checks for the filename. */
if (text->filepath == nullptr) {
BKE_report(op->reports, RPT_ERROR, "This text has not been saved");
return OPERATOR_CANCELLED;
}
if (!BKE_text_reload(text)) {
BKE_report(op->reports, RPT_ERROR, "Could not reopen file");
return OPERATOR_CANCELLED;
}
#ifdef WITH_PYTHON
if (text->compiled) {
BPY_text_free_code(text);
}
#endif
text_update_edited(text);
text_update_cursor_moved(C);
text_drawcache_tag_update(st, true);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
text->flags &= ~TXT_ISDIRTY;
/* return to scroll position */
st->top = orig_top;
txt_screen_clamp(st, region);
/* return cursor */
txt_move_to(text, orig_curl, orig_curc, false);
return OPERATOR_FINISHED;
}
void TEXT_OT_reload(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Reload";
ot->idname = "TEXT_OT_reload";
ot->description = "Reload active text data-block from its file";
/* api callbacks */
ot->exec = text_reload_exec;
ot->invoke = WM_operator_confirm;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Delete Operator
* \{ */
static bool text_unlink_poll(bContext *C)
{
/* it should be possible to unlink texts if they're lib-linked in... */
return CTX_data_edit_text(C) != nullptr;
}
static int text_unlink_exec(bContext *C, wmOperator * /*op*/)
{
Main *bmain = CTX_data_main(C);
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
/* make the previous text active, if its not there make the next text active */
if (st) {
if (text->id.prev) {
st->text = static_cast<Text *>(text->id.prev);
text_update_cursor_moved(C);
}
else if (text->id.next) {
st->text = static_cast<Text *>(text->id.next);
text_update_cursor_moved(C);
}
}
BKE_id_delete(bmain, text);
text_drawcache_tag_update(st, true);
WM_event_add_notifier(C, NC_TEXT | NA_REMOVED, nullptr);
return OPERATOR_FINISHED;
}
void TEXT_OT_unlink(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Unlink";
ot->idname = "TEXT_OT_unlink";
ot->description = "Unlink active text data-block";
/* api callbacks */
ot->exec = text_unlink_exec;
ot->invoke = WM_operator_confirm;
ot->poll = text_unlink_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Make Internal Operator
* \{ */
static int text_make_internal_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
text->flags |= TXT_ISMEM | TXT_ISDIRTY;
MEM_SAFE_FREE(text->filepath);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_make_internal(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Make Internal";
ot->idname = "TEXT_OT_make_internal";
ot->description = "Make active text file internal";
/* api callbacks */
ot->exec = text_make_internal_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Save Operator
* \{ */
static void txt_write_file(Main *bmain, Text *text, ReportList *reports)
{
FILE *fp;
BLI_stat_t st;
char filepath[FILE_MAX];
if (text->filepath == nullptr) {
BKE_reportf(reports, RPT_ERROR, "No file path for \"%s\"", text->id.name + 2);
return;
}
STRNCPY(filepath, text->filepath);
BLI_path_abs(filepath, BKE_main_blendfile_path(bmain));
/* Check if file write permission is ok. */
if (BLI_exists(filepath) && !BLI_file_is_writable(filepath)) {
BKE_reportf(
reports, RPT_ERROR, "Cannot save text file, path \"%s\" is not writable", filepath);
return;
}
fp = BLI_fopen(filepath, "w");
if (fp == nullptr) {
BKE_reportf(reports,
RPT_ERROR,
"Unable to save '%s': %s",
filepath,
errno ? strerror(errno) : TIP_("unknown error writing file"));
return;
}
LISTBASE_FOREACH (TextLine *, tmp, &text->lines) {
fputs(tmp->line, fp);
if (tmp->next) {
fputc('\n', fp);
}
}
fclose(fp);
if (BLI_stat(filepath, &st) == 0) {
text->mtime = st.st_mtime;
/* Report since this can be called from key shortcuts. */
BKE_reportf(reports, RPT_INFO, "Saved text \"%s\"", filepath);
}
else {
text->mtime = 0;
BKE_reportf(reports,
RPT_WARNING,
"Unable to stat '%s': %s",
filepath,
errno ? strerror(errno) : TIP_("unknown error statting file"));
}
text->flags &= ~TXT_ISDIRTY;
}
static int text_save_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Text *text = CTX_data_edit_text(C);
txt_write_file(bmain, text, op->reports);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
static int text_save_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
Text *text = CTX_data_edit_text(C);
/* Internal and texts without a filepath will go to "Save As". */
if (text->filepath == nullptr || (text->flags & TXT_ISMEM)) {
WM_operator_name_call(C, "TEXT_OT_save_as", WM_OP_INVOKE_DEFAULT, nullptr, event);
return OPERATOR_CANCELLED;
}
return text_save_exec(C, op);
}
void TEXT_OT_save(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Save";
ot->idname = "TEXT_OT_save";
ot->description = "Save active text data-block";
/* api callbacks */
ot->exec = text_save_exec;
ot->invoke = text_save_invoke;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Save As Operator
* \{ */
static int text_save_as_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Text *text = CTX_data_edit_text(C);
char filepath[FILE_MAX];
if (!text) {
return OPERATOR_CANCELLED;
}
RNA_string_get(op->ptr, "filepath", filepath);
if (text->filepath) {
MEM_freeN(text->filepath);
}
text->filepath = BLI_strdup(filepath);
text->flags &= ~TXT_ISMEM;
txt_write_file(bmain, text, op->reports);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
static int text_save_as_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
Main *bmain = CTX_data_main(C);
Text *text = CTX_data_edit_text(C);
if (RNA_struct_property_is_set(op->ptr, "filepath")) {
return text_save_as_exec(C, op);
}
const char *filepath;
if (text->filepath) {
filepath = text->filepath;
}
else if (text->flags & TXT_ISMEM) {
filepath = text->id.name + 2;
}
else {
filepath = BKE_main_blendfile_path(bmain);
}
RNA_string_set(op->ptr, "filepath", filepath);
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
void TEXT_OT_save_as(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Save As";
ot->idname = "TEXT_OT_save_as";
ot->description = "Save active text file with options";
/* api callbacks */
ot->exec = text_save_as_exec;
ot->invoke = text_save_as_invoke;
ot->poll = text_edit_poll;
/* properties */
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_TEXT | FILE_TYPE_PYSCRIPT,
FILE_SPECIAL,
FILE_SAVE,
WM_FILESEL_FILEPATH,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT); /* XXX TODO: relative_path. */
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Run Script Operator
* \{ */
static int text_run_script(bContext *C, ReportList *reports)
{
#ifdef WITH_PYTHON
Text *text = CTX_data_edit_text(C);
const bool is_live = (reports == nullptr);
/* only for comparison */
void *curl_prev = text->curl;
int curc_prev = text->curc;
if (BPY_run_text(C, text, reports, !is_live)) {
if (is_live) {
/* for nice live updates */
WM_event_add_notifier(C, NC_WINDOW | NA_EDITED, nullptr);
}
return OPERATOR_FINISHED;
}
/* Don't report error messages while live editing */
if (!is_live) {
/* text may have freed itself */
if (CTX_data_edit_text(C) == text) {
if (text->curl != curl_prev || curc_prev != text->curc) {
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
}
}
/* No need to report the error, this has already been handled by #BPY_run_text. */
return OPERATOR_FINISHED;
}
#else
(void)C;
(void)reports;
#endif /* !WITH_PYTHON */
return OPERATOR_CANCELLED;
}
static int text_run_script_exec(bContext *C, wmOperator *op)
{
#ifndef WITH_PYTHON
(void)C; /* unused */
BKE_report(op->reports, RPT_ERROR, "Python disabled in this build");
return OPERATOR_CANCELLED;
#else
return text_run_script(C, op->reports);
#endif
}
void TEXT_OT_run_script(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Run Script";
ot->idname = "TEXT_OT_run_script";
ot->description = "Run active script";
/* api callbacks */
ot->poll = text_data_poll;
ot->exec = text_run_script_exec;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Refresh Pyconstraints Operator
* \{ */
static int text_refresh_pyconstraints_exec(bContext * /*C*/, wmOperator * /*op*/)
{
#ifdef WITH_PYTHON
# if 0
Main *bmain = CTX_data_main(C);
Text *text = CTX_data_edit_text(C);
Object *ob;
bConstraint *con;
bool update;
/* check all pyconstraints */
for (ob = bmain->objects.first; ob; ob = ob->id.next) {
update = false;
if (ob->type == OB_ARMATURE && ob->pose) {
bPoseChannel *pchan;
for (pchan = ob->pose->chanbase.first; pchan; pchan = pchan->next) {
for (con = pchan->constraints.first; con; con = con->next) {
if (con->type == CONSTRAINT_TYPE_PYTHON) {
bPythonConstraint *data = con->data;
if (data->text == text) {
BPY_pyconstraint_update(ob, con);
}
update = true;
}
}
}
}
for (con = ob->constraints.first; con; con = con->next) {
if (con->type == CONSTRAINT_TYPE_PYTHON) {
bPythonConstraint *data = con->data;
if (data->text == text) {
BPY_pyconstraint_update(ob, con);
}
update = true;
}
}
if (update) {
DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY);
}
}
# endif
#endif
return OPERATOR_FINISHED;
}
void TEXT_OT_refresh_pyconstraints(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Refresh PyConstraints";
ot->idname = "TEXT_OT_refresh_pyconstraints";
ot->description = "Refresh all pyconstraints";
/* api callbacks */
ot->exec = text_refresh_pyconstraints_exec;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Paste Operator
* \{ */
static int text_paste_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
const bool selection = RNA_boolean_get(op->ptr, "selection");
char *buf;
int buf_len;
/* No need for UTF8 validation as the conversion handles invalid sequences gracefully. */
buf = WM_clipboard_text_get(selection, false, &buf_len);
if (!buf) {
return OPERATOR_CANCELLED;
}
text_drawcache_tag_update(st, false);
ED_text_undo_push_init(C);
/* Convert clipboard content indentation to spaces if specified */
if (text->flags & TXT_TABSTOSPACES) {
char *new_buf = buf_tabs_to_spaces(buf, TXT_TABSIZE, &buf_len);
MEM_freeN(buf);
buf = new_buf;
}
txt_insert_buf(text, buf, buf_len);
text_update_edited(text);
MEM_freeN(buf);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
/* run the script while editing, evil but useful */
if (st->live_edit) {
text_run_script(C, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_paste(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Paste";
ot->idname = "TEXT_OT_paste";
ot->description = "Paste text from clipboard";
/* api callbacks */
ot->exec = text_paste_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
PropertyRNA *prop;
prop = RNA_def_boolean(ot->srna,
"selection",
false,
"Selection",
"Paste text selected elsewhere rather than copied (X11/Wayland only)");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Duplicate Operator
* \{ */
static int text_duplicate_line_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
ED_text_undo_push_init(C);
txt_duplicate_line(text);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
/* run the script while editing, evil but useful */
if (CTX_wm_space_text(C)->live_edit) {
text_run_script(C, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_duplicate_line(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Duplicate Line";
ot->idname = "TEXT_OT_duplicate_line";
ot->description = "Duplicate the current line";
/* api callbacks */
ot->exec = text_duplicate_line_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Copy Operator
* \{ */
static void txt_copy_clipboard(Text *text)
{
char *buf;
if (!txt_has_sel(text)) {
return;
}
buf = txt_sel_to_buf(text, nullptr);
if (buf) {
WM_clipboard_text_set(buf, false);
MEM_freeN(buf);
}
}
static int text_copy_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
txt_copy_clipboard(text);
return OPERATOR_FINISHED;
}
void TEXT_OT_copy(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Copy";
ot->idname = "TEXT_OT_copy";
ot->description = "Copy selected text to clipboard";
/* api callbacks */
ot->exec = text_copy_exec;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Cut Operator
* \{ */
static int text_cut_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
text_drawcache_tag_update(st, false);
txt_copy_clipboard(text);
ED_text_undo_push_init(C);
txt_delete_selected(text);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
/* run the script while editing, evil but useful */
if (st->live_edit) {
text_run_script(C, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_cut(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Cut";
ot->idname = "TEXT_OT_cut";
ot->description = "Cut selected text to clipboard";
/* api callbacks */
ot->exec = text_cut_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Indent or Autocomplete Operator
* \{ */
static int text_indent_or_autocomplete_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
TextLine *line = text->curl;
bool text_before_cursor = text->curc != 0 && !ELEM(line->line[text->curc - 1], ' ', '\t');
if (text_before_cursor && (txt_has_sel(text) == false)) {
WM_operator_name_call(C, "TEXT_OT_autocomplete", WM_OP_INVOKE_DEFAULT, nullptr, nullptr);
}
else {
WM_operator_name_call(C, "TEXT_OT_indent", WM_OP_EXEC_DEFAULT, nullptr, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_indent_or_autocomplete(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Indent or Autocomplete";
ot->idname = "TEXT_OT_indent_or_autocomplete";
ot->description = "Indent selected text or autocomplete";
/* api callbacks */
ot->exec = text_indent_or_autocomplete_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = 0;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Indent Operator
* \{ */
static int text_indent_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
text_drawcache_tag_update(st, false);
ED_text_undo_push_init(C);
if (txt_has_sel(text)) {
txt_order_cursors(text, false);
txt_indent(text);
}
else {
txt_add_char(text, '\t');
}
text_update_edited(text);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_indent(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Indent";
ot->idname = "TEXT_OT_indent";
ot->description = "Indent selected text";
/* api callbacks */
ot->exec = text_indent_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Unindent Operator
* \{ */
static int text_unindent_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
text_drawcache_tag_update(st, false);
ED_text_undo_push_init(C);
txt_order_cursors(text, false);
txt_unindent(text);
text_update_edited(text);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_unindent(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Unindent";
ot->idname = "TEXT_OT_unindent";
ot->description = "Unindent selected text";
/* api callbacks */
ot->exec = text_unindent_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Line Break Operator
* \{ */
static int text_line_break_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
int a, curts;
int space = (text->flags & TXT_TABSTOSPACES) ? st->tabnumber : 1;
text_drawcache_tag_update(st, false);
/* Double check tabs/spaces before splitting the line. */
curts = txt_setcurr_tab_spaces(text, space);
ED_text_undo_push_init(C);
txt_split_curline(text);
for (a = 0; a < curts; a++) {
if (text->flags & TXT_TABSTOSPACES) {
txt_add_char(text, ' ');
}
else {
txt_add_char(text, '\t');
}
}
if (text->curl) {
if (text->curl->prev) {
text_update_line_edited(text->curl->prev);
}
text_update_line_edited(text->curl);
}
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_line_break(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Line Break";
ot->idname = "TEXT_OT_line_break";
ot->description = "Insert line break at cursor position";
/* api callbacks */
ot->exec = text_line_break_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Toggle-Comment Operator
* \{ */
static int text_comment_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
int type = RNA_enum_get(op->ptr, "type");
const char *prefix = ED_text_format_comment_line_prefix(text);
text_drawcache_tag_update(st, false);
ED_text_undo_push_init(C);
if (txt_has_sel(text)) {
txt_order_cursors(text, false);
}
switch (type) {
case 1:
txt_comment(text, prefix);
break;
case -1:
txt_uncomment(text, prefix);
break;
default:
if (txt_uncomment(text, prefix) == false) {
txt_comment(text, prefix);
}
break;
}
text_update_edited(text);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_comment_toggle(wmOperatorType *ot)
{
static const EnumPropertyItem comment_items[] = {
{0, "TOGGLE", 0, "Toggle Comments", nullptr},
{1, "COMMENT", 0, "Comment", nullptr},
{-1, "UNCOMMENT", 0, "Un-Comment", nullptr},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Toggle Comments";
ot->idname = "TEXT_OT_comment_toggle";
/* api callbacks */
ot->exec = text_comment_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
PropertyRNA *prop;
prop = RNA_def_enum(ot->srna, "type", comment_items, 0, "Type", "Add or remove comments");
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Convert Whitespace Operator
* \{ */
enum { TO_SPACES, TO_TABS };
static const EnumPropertyItem whitespace_type_items[] = {
{TO_SPACES, "SPACES", 0, "To Spaces", nullptr},
{TO_TABS, "TABS", 0, "To Tabs", nullptr},
{0, nullptr, 0, nullptr, nullptr},
};
static int text_convert_whitespace_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
FlattenString fs;
size_t a, j, max_len = 0;
int type = RNA_enum_get(op->ptr, "type");
const int curc_column = text->curl ?
BLI_str_utf8_offset_to_column_with_tabs(
text->curl->line, text->curl->len, text->curc, TXT_TABSIZE) :
-1;
const int selc_column = text->sell ?
BLI_str_utf8_offset_to_column_with_tabs(
text->sell->line, text->sell->len, text->selc, TXT_TABSIZE) :
-1;
/* first convert to all space, this make it a lot easier to convert to tabs
* because there is no mixtures of ' ' && '\t' */
LISTBASE_FOREACH (TextLine *, tmp, &text->lines) {
char *new_line;
BLI_assert(tmp->line);
flatten_string(st, &fs, tmp->line);
new_line = BLI_strdup(fs.buf);
flatten_string_free(&fs);
MEM_freeN(tmp->line);
if (tmp->format) {
MEM_freeN(tmp->format);
}
/* Put new_line in the tmp->line spot still need to try and set the curc correctly. */
tmp->line = new_line;
tmp->len = strlen(new_line);
tmp->format = nullptr;
if (tmp->len > max_len) {
max_len = tmp->len;
}
}
if (type == TO_TABS) {
char *tmp_line = static_cast<char *>(MEM_mallocN(sizeof(*tmp_line) * (max_len + 1), __func__));
LISTBASE_FOREACH (TextLine *, tmp, &text->lines) {
const char *text_check_line = tmp->line;
const int text_check_line_len = tmp->len;
char *tmp_line_cur = tmp_line;
const size_t tab_len = st->tabnumber;
BLI_assert(text_check_line);
for (a = 0; a < text_check_line_len;) {
/* A tab can only start at a position multiple of tab_len... */
if (!(a % tab_len) && (text_check_line[a] == ' ')) {
/* a + 0 we already know to be ' ' char... */
for (j = 1;
(j < tab_len) && (a + j < text_check_line_len) && (text_check_line[a + j] == ' ');
j++) {
/* pass */
}
if (j == tab_len) {
/* We found a set of spaces that can be replaced by a tab... */
if ((tmp_line_cur == tmp_line) && a != 0) {
/* Copy all 'valid' string already 'parsed'... */
memcpy(tmp_line_cur, text_check_line, a);
tmp_line_cur += a;
}
*tmp_line_cur = '\t';
tmp_line_cur++;
a += j;
}
else {
if (tmp_line_cur != tmp_line) {
memcpy(tmp_line_cur, &text_check_line[a], j);
tmp_line_cur += j;
}
a += j;
}
}
else {
size_t len = BLI_str_utf8_size_safe(&text_check_line[a]);
if (tmp_line_cur != tmp_line) {
memcpy(tmp_line_cur, &text_check_line[a], len);
tmp_line_cur += len;
}
a += len;
}
}
if (tmp_line_cur != tmp_line) {
*tmp_line_cur = '\0';
#ifndef NDEBUG
BLI_assert(tmp_line_cur - tmp_line <= max_len);
flatten_string(st, &fs, tmp_line);
BLI_assert(STREQ(fs.buf, tmp->line));
flatten_string_free(&fs);
#endif
MEM_freeN(tmp->line);
if (tmp->format) {
MEM_freeN(tmp->format);
}
/* Put new_line in the `tmp->line` spot. */
tmp->len = strlen(tmp_line);
tmp->line = BLI_strdupn(tmp_line, tmp->len);
tmp->format = nullptr;
}
}
MEM_freeN(tmp_line);
}
if (curc_column != -1) {
text->curc = BLI_str_utf8_offset_from_column_with_tabs(
text->curl->line, text->curl->len, curc_column, TXT_TABSIZE);
}
if (selc_column != -1) {
text->selc = BLI_str_utf8_offset_from_column_with_tabs(
text->sell->line, text->sell->len, selc_column, TXT_TABSIZE);
}
text_update_edited(text);
text_update_cursor_moved(C);
text_drawcache_tag_update(st, true);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_convert_whitespace(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Convert Whitespace";
ot->idname = "TEXT_OT_convert_whitespace";
ot->description = "Convert whitespaces by type";
/* api callbacks */
ot->exec = text_convert_whitespace_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
RNA_def_enum(ot->srna,
"type",
whitespace_type_items,
TO_SPACES,
"Type",
"Type of whitespace to convert to");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Select All Operator
* \{ */
static int text_select_all_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
txt_sel_all(text);
text_update_cursor_moved(C);
text_select_update_primary_clipboard(text);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_select_all(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Select All";
ot->idname = "TEXT_OT_select_all";
ot->description = "Select all text";
/* api callbacks */
ot->exec = text_select_all_exec;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Select Line Operator
* \{ */
static int text_select_line_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
txt_sel_line(text);
text_update_cursor_moved(C);
text_select_update_primary_clipboard(text);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_select_line(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Select Line";
ot->idname = "TEXT_OT_select_line";
ot->description = "Select text by line";
/* api callbacks */
ot->exec = text_select_line_exec;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Select Word Operator
* \{ */
static int text_select_word_exec(bContext *C, wmOperator * /*op*/)
{
Text *text = CTX_data_edit_text(C);
BLI_str_cursor_step_bounds_utf8(
text->curl->line, text->curl->len, text->selc, &text->curc, &text->selc);
text_update_cursor_moved(C);
text_select_update_primary_clipboard(text);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_select_word(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Select Word";
ot->idname = "TEXT_OT_select_word";
ot->description = "Select word under cursor";
/* api callbacks */
ot->exec = text_select_word_exec;
ot->poll = text_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Move Lines Operators
* \{ */
static int move_lines_exec(bContext *C, wmOperator *op)
{
Text *text = CTX_data_edit_text(C);
const int direction = RNA_enum_get(op->ptr, "direction");
ED_text_undo_push_init(C);
txt_move_lines(text, direction);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
/* run the script while editing, evil but useful */
if (CTX_wm_space_text(C)->live_edit) {
text_run_script(C, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_move_lines(wmOperatorType *ot)
{
static const EnumPropertyItem direction_items[] = {
{TXT_MOVE_LINE_UP, "UP", 0, "Up", ""},
{TXT_MOVE_LINE_DOWN, "DOWN", 0, "Down", ""},
{0, nullptr, 0, nullptr, nullptr},
};
/* identifiers */
ot->name = "Move Lines";
ot->idname = "TEXT_OT_move_lines";
ot->description = "Move the currently selected line(s) up/down";
/* api callbacks */
ot->exec = move_lines_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
RNA_def_enum(ot->srna, "direction", direction_items, 1, "Direction", "");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Move Operator
* \{ */
static const EnumPropertyItem move_type_items[] = {
{LINE_BEGIN, "LINE_BEGIN", 0, "Line Begin", ""},
{LINE_END, "LINE_END", 0, "Line End", ""},
{FILE_TOP, "FILE_TOP", 0, "File Top", ""},
{FILE_BOTTOM, "FILE_BOTTOM", 0, "File Bottom", ""},
{PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
{NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
{PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
{NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
{PREV_LINE, "PREVIOUS_LINE", 0, "Previous Line", ""},
{NEXT_LINE, "NEXT_LINE", 0, "Next Line", ""},
{PREV_PAGE, "PREVIOUS_PAGE", 0, "Previous Page", ""},
{NEXT_PAGE, "NEXT_PAGE", 0, "Next Page", ""},
{0, nullptr, 0, nullptr, nullptr},
};
/* get cursor position in line by relative wrapped line and column positions */
static int text_get_cursor_rel(
SpaceText *st, ARegion *region, TextLine *linein, int rell, int relc)
{
int i, j, start, end, max, curs, endj, selc;
bool chop, loop, found;
char ch;
max = wrap_width(st, region);
selc = start = endj = curs = found = false;
end = max;
chop = loop = true;
for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe(linein->line + j)) {
int chars;
int columns = BLI_str_utf8_char_width_safe(linein->line + j); /* = 1 for tab */
/* Mimic replacement of tabs */
ch = linein->line[j];
if (ch == '\t') {
chars = st->tabnumber - i % st->tabnumber;
ch = ' ';
}
else {
chars = 1;
}
while (chars--) {
if (rell == 0 && i - start <= relc && i + columns - start > relc) {
/* current position could be wrapped to next line */
/* this should be checked when end of current line would be reached */
selc = j;
found = true;
}
else if (i - end <= relc && i + columns - end > relc) {
curs = j;
}
if (i + columns - start > max) {
end = MIN2(end, i);
if (found) {
/* exact cursor position was found, check if it's */
/* still on needed line (hasn't been wrapped) */
if (selc > endj && !chop) {
selc = endj;
}
loop = false;
break;
}
if (chop) {
endj = j;
}
start = end;
end += max;
chop = true;
rell--;
if (rell == 0 && i + columns - start > relc) {
selc = curs;
loop = false;
break;
}
}
else if (ch == '\0') {
if (!found) {
selc = linein->len;
}
loop = false;
break;
}
else if (ELEM(ch, ' ', '-')) {
if (found) {
loop = false;
break;
}
if (rell == 0 && i + columns - start > relc) {
selc = curs;
loop = false;
break;
}
end = i + 1;
endj = j;
chop = false;
}
i += columns;
}
}
return selc;
}
static int cursor_skip_find_line(
SpaceText *st, ARegion *region, int lines, TextLine **linep, int *charp, int *rell, int *relc)
{
int offl, offc, visible_lines;
wrap_offset_in_line(st, region, *linep, *charp, &offl, &offc);
*relc = text_get_char_pos(st, (*linep)->line, *charp) + offc;
*rell = lines;
/* handle current line */
if (lines > 0) {
visible_lines = text_get_visible_lines(st, region, (*linep)->line);
if (*rell - visible_lines + offl >= 0) {
if (!(*linep)->next) {
if (offl < visible_lines - 1) {
*rell = visible_lines - 1;
return 1;
}
*charp = (*linep)->len;
return 0;
}
*rell -= visible_lines - offl;
*linep = (*linep)->next;
}
else {
*rell += offl;
return 1;
}
}
else {
if (*rell + offl <= 0) {
if (!(*linep)->prev) {
if (offl) {
*rell = 0;
return 1;
}
*charp = 0;
return 0;
}
*rell += offl;
*linep = (*linep)->prev;
}
else {
*rell += offl;
return 1;
}
}
/* skip lines and find destination line and offsets */
while (*linep) {
visible_lines = text_get_visible_lines(st, region, (*linep)->line);
if (lines < 0) { /* moving top */
if (*rell + visible_lines >= 0) {
*rell += visible_lines;
break;
}
if (!(*linep)->prev) {
*rell = 0;
break;
}
*rell += visible_lines;
*linep = (*linep)->prev;
}
else { /* moving bottom */
if (*rell - visible_lines < 0) {
break;
}
if (!(*linep)->next) {
*rell = visible_lines - 1;
break;
}
*rell -= visible_lines;
*linep = (*linep)->next;
}
}
return 1;
}
static void txt_wrap_move_bol(SpaceText *st, ARegion *region, const bool sel)
{
Text *text = st->text;
TextLine **linep;
int *charp;
int oldc, i, j, max, start, end, endj;
bool chop, loop;
char ch;
text_update_character_width(st);
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
oldc = *charp;
max = wrap_width(st, region);
start = endj = 0;
end = max;
chop = loop = true;
*charp = 0;
for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe((*linep)->line + j)) {
int chars;
int columns = BLI_str_utf8_char_width_safe((*linep)->line + j); /* = 1 for tab */
/* Mimic replacement of tabs */
ch = (*linep)->line[j];
if (ch == '\t') {
chars = st->tabnumber - i % st->tabnumber;
ch = ' ';
}
else {
chars = 1;
}
while (chars--) {
if (i + columns - start > max) {
end = MIN2(end, i);
*charp = endj;
if (j >= oldc) {
if (ch == '\0') {
*charp = BLI_str_utf8_offset_from_column_with_tabs(
(*linep)->line, (*linep)->len, start, TXT_TABSIZE);
}
loop = false;
break;
}
if (chop) {
endj = j;
}
start = end;
end += max;
chop = true;
}
else if (ELEM(ch, ' ', '-', '\0')) {
if (j >= oldc) {
*charp = BLI_str_utf8_offset_from_column_with_tabs(
(*linep)->line, (*linep)->len, start, TXT_TABSIZE);
loop = false;
break;
}
end = i + 1;
endj = j + 1;
chop = false;
}
i += columns;
}
}
if (!sel) {
txt_pop_sel(text);
}
}
static void txt_wrap_move_eol(SpaceText *st, ARegion *region, const bool sel)
{
Text *text = st->text;
TextLine **linep;
int *charp;
int oldc, i, j, max, start, end, endj;
bool chop, loop;
char ch;
text_update_character_width(st);
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
oldc = *charp;
max = wrap_width(st, region);
start = endj = 0;
end = max;
chop = loop = true;
*charp = 0;
for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe((*linep)->line + j)) {
int chars;
int columns = BLI_str_utf8_char_width_safe((*linep)->line + j); /* = 1 for tab */
/* Mimic replacement of tabs */
ch = (*linep)->line[j];
if (ch == '\t') {
chars = st->tabnumber - i % st->tabnumber;
ch = ' ';
}
else {
chars = 1;
}
while (chars--) {
if (i + columns - start > max) {
end = MIN2(end, i);
if (chop) {
endj = BLI_str_find_prev_char_utf8((*linep)->line + j, (*linep)->line) - (*linep)->line;
}
if (endj >= oldc) {
if (ch == '\0') {
*charp = (*linep)->len;
}
else {
*charp = endj;
}
loop = false;
break;
}
start = end;
end += max;
chop = true;
}
else if (ch == '\0') {
*charp = (*linep)->len;
loop = false;
break;
}
else if (ELEM(ch, ' ', '-')) {
end = i + 1;
endj = j;
chop = false;
}
i += columns;
}
}
if (!sel) {
txt_pop_sel(text);
}
}
static void txt_wrap_move_up(SpaceText *st, ARegion *region, const bool sel)
{
Text *text = st->text;
TextLine **linep;
int *charp;
int offl, offc, col;
text_update_character_width(st);
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
wrap_offset_in_line(st, region, *linep, *charp, &offl, &offc);
col = text_get_char_pos(st, (*linep)->line, *charp) + offc;
if (offl) {
*charp = text_get_cursor_rel(st, region, *linep, offl - 1, col);
}
else {
if ((*linep)->prev) {
int visible_lines;
*linep = (*linep)->prev;
visible_lines = text_get_visible_lines(st, region, (*linep)->line);
*charp = text_get_cursor_rel(st, region, *linep, visible_lines - 1, col);
}
else {
*charp = 0;
}
}
if (!sel) {
txt_pop_sel(text);
}
}
static void txt_wrap_move_down(SpaceText *st, ARegion *region, const bool sel)
{
Text *text = st->text;
TextLine **linep;
int *charp;
int offl, offc, col, visible_lines;
text_update_character_width(st);
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
wrap_offset_in_line(st, region, *linep, *charp, &offl, &offc);
col = text_get_char_pos(st, (*linep)->line, *charp) + offc;
visible_lines = text_get_visible_lines(st, region, (*linep)->line);
if (offl < visible_lines - 1) {
*charp = text_get_cursor_rel(st, region, *linep, offl + 1, col);
}
else {
if ((*linep)->next) {
*linep = (*linep)->next;
*charp = text_get_cursor_rel(st, region, *linep, 0, col);
}
else {
*charp = (*linep)->len;
}
}
if (!sel) {
txt_pop_sel(text);
}
}
/* Moves the cursor vertically by the specified number of lines.
* If the destination line is shorter than the current cursor position, the
* cursor will be positioned at the end of this line.
*
* This is to replace screen_skip for PageUp/Down operations.
*/
static void cursor_skip(SpaceText *st, ARegion *region, Text *text, int lines, const bool sel)
{
TextLine **linep;
int *charp;
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
if (st && region && st->wordwrap) {
int rell, relc;
/* find line and offsets inside it needed to set cursor position */
if (cursor_skip_find_line(st, region, lines, linep, charp, &rell, &relc)) {
*charp = text_get_cursor_rel(st, region, *linep, rell, relc);
}
}
else {
while (lines > 0 && (*linep)->next) {
*linep = (*linep)->next;
lines--;
}
while (lines < 0 && (*linep)->prev) {
*linep = (*linep)->prev;
lines++;
}
}
if (*charp > (*linep)->len) {
*charp = (*linep)->len;
}
if (!sel) {
txt_pop_sel(text);
}
}
static int text_move_cursor(bContext *C, int type, bool select)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
ARegion *region = CTX_wm_region(C);
/* ensure we have the right region, it's optional */
if (region && region->regiontype != RGN_TYPE_WINDOW) {
region = nullptr;
}
switch (type) {
case LINE_BEGIN:
if (!select) {
txt_sel_clear(text);
}
if (st && st->wordwrap && region) {
txt_wrap_move_bol(st, region, select);
}
else {
txt_move_bol(text, select);
}
break;
case LINE_END:
if (!select) {
txt_sel_clear(text);
}
if (st && st->wordwrap && region) {
txt_wrap_move_eol(st, region, select);
}
else {
txt_move_eol(text, select);
}
break;
case FILE_TOP:
txt_move_bof(text, select);
break;
case FILE_BOTTOM:
txt_move_eof(text, select);
break;
case PREV_WORD:
if (txt_cursor_is_line_start(text)) {
txt_move_left(text, select);
}
txt_jump_left(text, select, true);
break;
case NEXT_WORD:
if (txt_cursor_is_line_end(text)) {
txt_move_right(text, select);
}
txt_jump_right(text, select, true);
break;
case PREV_CHAR:
if (txt_has_sel(text) && !select) {
txt_order_cursors(text, false);
txt_pop_sel(text);
}
else {
txt_move_left(text, select);
}
break;
case NEXT_CHAR:
if (txt_has_sel(text) && !select) {
txt_order_cursors(text, true);
txt_pop_sel(text);
}
else {
txt_move_right(text, select);
}
break;
case PREV_LINE:
if (st && st->wordwrap && region) {
txt_wrap_move_up(st, region, select);
}
else {
txt_move_up(text, select);
}
break;
case NEXT_LINE:
if (st && st->wordwrap && region) {
txt_wrap_move_down(st, region, select);
}
else {
txt_move_down(text, select);
}
break;
case PREV_PAGE:
if (st) {
cursor_skip(st, region, st->text, -st->runtime.viewlines, select);
}
else {
cursor_skip(nullptr, nullptr, text, -10, select);
}
break;
case NEXT_PAGE:
if (st) {
cursor_skip(st, region, st->text, st->runtime.viewlines, select);
}
else {
cursor_skip(nullptr, nullptr, text, 10, select);
}
break;
}
text_update_cursor_moved(C);
if (select) {
text_select_update_primary_clipboard(st->text);
}
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
return OPERATOR_FINISHED;
}
static int text_move_exec(bContext *C, wmOperator *op)
{
int type = RNA_enum_get(op->ptr, "type");
return text_move_cursor(C, type, false);
}
void TEXT_OT_move(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Move Cursor";
ot->idname = "TEXT_OT_move";
ot->description = "Move cursor to position type";
/* api callbacks */
ot->exec = text_move_exec;
ot->poll = text_edit_poll;
/* properties */
RNA_def_enum(ot->srna, "type", move_type_items, LINE_BEGIN, "Type", "Where to move cursor to");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Move Select Operator
* \{ */
static int text_move_select_exec(bContext *C, wmOperator *op)
{
int type = RNA_enum_get(op->ptr, "type");
return text_move_cursor(C, type, true);
}
void TEXT_OT_move_select(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Move Select";
ot->idname = "TEXT_OT_move_select";
ot->description = "Move the cursor while selecting";
/* api callbacks */
ot->exec = text_move_select_exec;
ot->poll = text_space_edit_poll;
/* properties */
RNA_def_enum(ot->srna,
"type",
move_type_items,
LINE_BEGIN,
"Type",
"Where to move cursor to, to make a selection");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Jump Operator
* \{ */
static int text_jump_exec(bContext *C, wmOperator *op)
{
Text *text = CTX_data_edit_text(C);
int line = RNA_int_get(op->ptr, "line");
short nlines = txt_get_span(static_cast<TextLine *>(text->lines.first),
static_cast<TextLine *>(text->lines.last)) +
1;
if (line < 1) {
txt_move_toline(text, 1, false);
}
else if (line > nlines) {
txt_move_toline(text, nlines - 1, false);
}
else {
txt_move_toline(text, line - 1, false);
}
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
return OPERATOR_FINISHED;
}
static int text_jump_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
return WM_operator_props_dialog_popup(C, op, 200);
}
void TEXT_OT_jump(wmOperatorType *ot)
{
PropertyRNA *prop;
/* identifiers */
ot->name = "Jump";
ot->idname = "TEXT_OT_jump";
ot->description = "Jump cursor to line";
/* api callbacks */
ot->invoke = text_jump_invoke;
ot->exec = text_jump_exec;
ot->poll = text_edit_poll;
/* properties */
prop = RNA_def_int(ot->srna, "line", 1, 1, INT_MAX, "Line", "Line number to jump to", 1, 10000);
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_TEXT);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Delete Operator
* \{ */
static const EnumPropertyItem delete_type_items[] = {
{DEL_NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
{DEL_PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
{DEL_NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
{DEL_PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static int text_delete_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
int type = RNA_enum_get(op->ptr, "type");
text_drawcache_tag_update(st, true);
/* behavior could be changed here,
* but for now just don't jump words when we have a selection */
if (txt_has_sel(text)) {
if (type == DEL_PREV_WORD) {
type = DEL_PREV_CHAR;
}
else if (type == DEL_NEXT_WORD) {
type = DEL_NEXT_CHAR;
}
}
ED_text_undo_push_init(C);
if (type == DEL_PREV_WORD) {
if (txt_cursor_is_line_start(text)) {
txt_backspace_char(text);
}
txt_backspace_word(text);
}
else if (type == DEL_PREV_CHAR) {
if (text->flags & TXT_TABSTOSPACES) {
if (!txt_has_sel(text) && !txt_cursor_is_line_start(text)) {
int tabsize = 0;
tabsize = txt_calc_tab_left(text->curl, text->curc);
if (tabsize) {
text->sell = text->curl;
text->selc = text->curc - tabsize;
txt_order_cursors(text, false);
}
}
}
if (U.text_flag & USER_TEXT_EDIT_AUTO_CLOSE) {
const char *curr = text->curl->line + text->curc;
if (*curr != '\0') {
const char *prev = BLI_str_find_prev_char_utf8(curr, text->curl->line);
if ((curr != prev) && /* When back-spacing from the start of the line. */
(*curr == text_closing_character_pair_get(*prev)))
{
txt_move_right(text, false);
txt_backspace_char(text);
}
}
}
txt_backspace_char(text);
}
else if (type == DEL_NEXT_WORD) {
if (txt_cursor_is_line_end(text)) {
txt_delete_char(text);
}
txt_delete_word(text);
}
else if (type == DEL_NEXT_CHAR) {
if (text->flags & TXT_TABSTOSPACES) {
if (!txt_has_sel(text) && !txt_cursor_is_line_end(text)) {
int tabsize = 0;
tabsize = txt_calc_tab_right(text->curl, text->curc);
if (tabsize) {
text->sell = text->curl;
text->selc = text->curc + tabsize;
txt_order_cursors(text, true);
}
}
}
txt_delete_char(text);
}
text_update_line_edited(text->curl);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
/* run the script while editing, evil but useful */
if (st->live_edit) {
text_run_script(C, nullptr);
}
return OPERATOR_FINISHED;
}
void TEXT_OT_delete(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Delete";
ot->idname = "TEXT_OT_delete";
ot->description = "Delete text by cursor position";
/* api callbacks */
ot->exec = text_delete_exec;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
PropertyRNA *prop;
prop = RNA_def_enum(ot->srna,
"type",
delete_type_items,
DEL_NEXT_CHAR,
"Type",
"Which part of the text to delete");
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Toggle Overwrite Operator
* \{ */
static int text_toggle_overwrite_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
st->overwrite = !st->overwrite;
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
return OPERATOR_FINISHED;
}
void TEXT_OT_overwrite_toggle(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Toggle Overwrite";
ot->idname = "TEXT_OT_overwrite_toggle";
ot->description = "Toggle overwrite while typing";
/* api callbacks */
ot->exec = text_toggle_overwrite_exec;
ot->poll = text_space_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Scroll Operator
* \{ */
static void txt_screen_clamp(SpaceText *st, ARegion *region)
{
if (st->top <= 0) {
st->top = 0;
}
else {
int last;
last = text_get_total_lines(st, region);
last = last - (st->runtime.viewlines / 2);
if (last > 0 && st->top > last) {
st->top = last;
}
}
}
/* Moves the view vertically by the specified number of lines */
static void txt_screen_skip(SpaceText *st, ARegion *region, int lines)
{
st->top += lines;
txt_screen_clamp(st, region);
}
/* quick enum for tsc->zone (scroller handles) */
enum eScrollZone {
SCROLLHANDLE_INVALID_OUTSIDE = -1,
SCROLLHANDLE_BAR,
SCROLLHANDLE_MIN_OUTSIDE,
SCROLLHANDLE_MAX_OUTSIDE,
};
struct TextScroll {
int mval_prev[2];
int mval_delta[2];
bool is_first;
bool is_scrollbar;
enum eScrollZone zone;
/* Store the state of the display, cache some constant vars. */
struct {
int ofs_init[2];
int ofs_max[2];
int size_px[2];
} state;
int ofs_delta[2];
int ofs_delta_px[2];
};
static void text_scroll_state_init(TextScroll *tsc, SpaceText *st, ARegion *region)
{
tsc->state.ofs_init[0] = st->left;
tsc->state.ofs_init[1] = st->top;
tsc->state.ofs_max[0] = INT_MAX;
tsc->state.ofs_max[1] = max_ii(0,
text_get_total_lines(st, region) - (st->runtime.viewlines / 2));
tsc->state.size_px[0] = st->runtime.cwidth_px;
tsc->state.size_px[1] = TXT_LINE_HEIGHT(st);
}
static bool text_scroll_poll(bContext *C)
{
/* it should be possible to still scroll linked texts to read them,
* even if they can't be edited... */
return CTX_data_edit_text(C) != nullptr;
}
static int text_scroll_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
int lines = RNA_int_get(op->ptr, "lines");
if (lines == 0) {
return OPERATOR_CANCELLED;
}
txt_screen_skip(st, region, lines * 3);
ED_area_tag_redraw(CTX_wm_area(C));
return OPERATOR_FINISHED;
}
static void text_scroll_apply(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
TextScroll *tsc = static_cast<TextScroll *>(op->customdata);
const int mval[2] = {event->xy[0], event->xy[1]};
text_update_character_width(st);
/* compute mouse move distance */
if (tsc->is_first) {
copy_v2_v2_int(tsc->mval_prev, mval);
tsc->is_first = false;
}
if (event->type != MOUSEPAN) {
sub_v2_v2v2_int(tsc->mval_delta, mval, tsc->mval_prev);
}
/* accumulate scroll, in float values for events that give less than one
* line offset but taken together should still scroll */
if (!tsc->is_scrollbar) {
tsc->ofs_delta_px[0] -= tsc->mval_delta[0];
tsc->ofs_delta_px[1] += tsc->mval_delta[1];
}
else {
tsc->ofs_delta_px[1] -= (tsc->mval_delta[1] * st->runtime.scroll_px_per_line) *
tsc->state.size_px[1];
}
for (int i = 0; i < 2; i += 1) {
int lines_from_pixels = tsc->ofs_delta_px[i] / tsc->state.size_px[i];
tsc->ofs_delta[i] += lines_from_pixels;
tsc->ofs_delta_px[i] -= lines_from_pixels * tsc->state.size_px[i];
}
/* The final values need to be calculated from the inputs,
* so clamping and ensuring an unsigned pixel offset doesn't conflict with
* updating the cursor mval_delta. */
int scroll_ofs_new[2] = {
tsc->state.ofs_init[0] + tsc->ofs_delta[0],
tsc->state.ofs_init[1] + tsc->ofs_delta[1],
};
int scroll_ofs_px_new[2] = {
tsc->ofs_delta_px[0],
tsc->ofs_delta_px[1],
};
for (int i = 0; i < 2; i += 1) {
/* Ensure always unsigned (adjusting line/column accordingly). */
while (scroll_ofs_px_new[i] < 0) {
scroll_ofs_px_new[i] += tsc->state.size_px[i];
scroll_ofs_new[i] -= 1;
}
/* Clamp within usable region. */
if (scroll_ofs_new[i] < 0) {
scroll_ofs_new[i] = 0;
scroll_ofs_px_new[i] = 0;
}
else if (scroll_ofs_new[i] >= tsc->state.ofs_max[i]) {
scroll_ofs_new[i] = tsc->state.ofs_max[i];
scroll_ofs_px_new[i] = 0;
}
}
/* Override for word-wrap. */
if (st->wordwrap) {
scroll_ofs_new[0] = 0;
scroll_ofs_px_new[0] = 0;
}
/* Apply to the screen. */
if (scroll_ofs_new[0] != st->left || scroll_ofs_new[1] != st->top ||
/* Horizontal sub-pixel offset currently isn't used. */
/* scroll_ofs_px_new[0] != st->scroll_ofs_px[0] || */
scroll_ofs_px_new[1] != st->runtime.scroll_ofs_px[1])
{
st->left = scroll_ofs_new[0];
st->top = scroll_ofs_new[1];
st->runtime.scroll_ofs_px[0] = scroll_ofs_px_new[0];
st->runtime.scroll_ofs_px[1] = scroll_ofs_px_new[1];
ED_area_tag_redraw(CTX_wm_area(C));
}
tsc->mval_prev[0] = mval[0];
tsc->mval_prev[1] = mval[1];
}
static void scroll_exit(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
TextScroll *tsc = static_cast<TextScroll *>(op->customdata);
st->flags &= ~ST_SCROLL_SELECT;
if (st->runtime.scroll_ofs_px[1] > tsc->state.size_px[1] / 2) {
st->top += 1;
}
st->runtime.scroll_ofs_px[0] = 0;
st->runtime.scroll_ofs_px[1] = 0;
ED_area_tag_redraw(CTX_wm_area(C));
MEM_freeN(op->customdata);
}
static int text_scroll_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
TextScroll *tsc = static_cast<TextScroll *>(op->customdata);
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
switch (event->type) {
case MOUSEMOVE:
if (tsc->zone == SCROLLHANDLE_BAR) {
text_scroll_apply(C, op, event);
}
break;
case LEFTMOUSE:
case RIGHTMOUSE:
case MIDDLEMOUSE:
if (event->val == KM_RELEASE) {
if (ELEM(tsc->zone, SCROLLHANDLE_MIN_OUTSIDE, SCROLLHANDLE_MAX_OUTSIDE)) {
txt_screen_skip(st,
region,
st->runtime.viewlines *
(tsc->zone == SCROLLHANDLE_MIN_OUTSIDE ? 1 : -1));
ED_area_tag_redraw(CTX_wm_area(C));
}
scroll_exit(C, op);
return OPERATOR_FINISHED;
}
}
return OPERATOR_RUNNING_MODAL;
}
static void text_scroll_cancel(bContext *C, wmOperator *op)
{
scroll_exit(C, op);
}
static int text_scroll_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
TextScroll *tsc;
if (RNA_struct_property_is_set(op->ptr, "lines")) {
return text_scroll_exec(C, op);
}
tsc = static_cast<TextScroll *>(MEM_callocN(sizeof(TextScroll), "TextScroll"));
tsc->is_first = true;
tsc->zone = SCROLLHANDLE_BAR;
text_scroll_state_init(tsc, st, region);
op->customdata = tsc;
st->flags |= ST_SCROLL_SELECT;
if (event->type == MOUSEPAN) {
text_update_character_width(st);
copy_v2_v2_int(tsc->mval_prev, event->xy);
/* Sensitivity of scroll set to 4pix per line/char */
tsc->mval_delta[0] = (event->xy[0] - event->prev_xy[0]) * st->runtime.cwidth_px / 4;
tsc->mval_delta[1] = (event->xy[1] - event->prev_xy[1]) * st->runtime.lheight_px / 4;
tsc->is_first = false;
tsc->is_scrollbar = false;
text_scroll_apply(C, op, event);
scroll_exit(C, op);
return OPERATOR_FINISHED;
}
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
}
void TEXT_OT_scroll(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Scroll";
/* don't really see the difference between this and
* scroll_bar. Both do basically the same thing (aside from key-maps). */
ot->idname = "TEXT_OT_scroll";
/* api callbacks */
ot->exec = text_scroll_exec;
ot->invoke = text_scroll_invoke;
ot->modal = text_scroll_modal;
ot->cancel = text_scroll_cancel;
ot->poll = text_scroll_poll;
/* flags */
ot->flag = OPTYPE_BLOCKING | OPTYPE_GRAB_CURSOR_XY | OPTYPE_INTERNAL;
/* properties */
PropertyRNA *prop;
prop = RNA_def_int(
ot->srna, "lines", 1, INT_MIN, INT_MAX, "Lines", "Number of lines to scroll", -100, 100);
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_TEXT);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Scroll Bar Operator
* \{ */
static bool text_region_scroll_poll(bContext *C)
{
/* same as text_region_edit_poll except it works on libdata too */
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
ARegion *region = CTX_wm_region(C);
if (!st || !text) {
return false;
}
if (!region || region->regiontype != RGN_TYPE_WINDOW) {
return false;
}
return true;
}
static int text_scroll_bar_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
TextScroll *tsc;
const int *mval = event->mval;
enum eScrollZone zone = SCROLLHANDLE_INVALID_OUTSIDE;
if (RNA_struct_property_is_set(op->ptr, "lines")) {
return text_scroll_exec(C, op);
}
/* verify we are in the right zone */
if (mval[0] > st->runtime.scroll_region_handle.xmin &&
mval[0] < st->runtime.scroll_region_handle.xmax)
{
if (mval[1] >= st->runtime.scroll_region_handle.ymin &&
mval[1] <= st->runtime.scroll_region_handle.ymax)
{
/* mouse inside scroll handle */
zone = SCROLLHANDLE_BAR;
}
else if (mval[1] > TXT_SCROLL_SPACE && mval[1] < region->winy - TXT_SCROLL_SPACE) {
if (mval[1] < st->runtime.scroll_region_handle.ymin) {
zone = SCROLLHANDLE_MIN_OUTSIDE;
}
else {
zone = SCROLLHANDLE_MAX_OUTSIDE;
}
}
}
if (zone == SCROLLHANDLE_INVALID_OUTSIDE) {
/* we are outside slider - nothing to do */
return OPERATOR_PASS_THROUGH;
}
tsc = static_cast<TextScroll *>(MEM_callocN(sizeof(TextScroll), "TextScroll"));
tsc->is_first = true;
tsc->is_scrollbar = true;
tsc->zone = zone;
op->customdata = tsc;
st->flags |= ST_SCROLL_SELECT;
text_scroll_state_init(tsc, st, region);
/* jump scroll, works in v2d but needs to be added here too :S */
if (event->type == MIDDLEMOUSE) {
tsc->mval_prev[0] = region->winrct.xmin + BLI_rcti_cent_x(&st->runtime.scroll_region_handle);
tsc->mval_prev[1] = region->winrct.ymin + BLI_rcti_cent_y(&st->runtime.scroll_region_handle);
tsc->is_first = false;
tsc->zone = SCROLLHANDLE_BAR;
text_scroll_apply(C, op, event);
}
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
}
void TEXT_OT_scroll_bar(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Scrollbar";
/* don't really see the difference between this and
* scroll. Both do basically the same thing (aside from key-maps). */
ot->idname = "TEXT_OT_scroll_bar";
/* api callbacks */
ot->invoke = text_scroll_bar_invoke;
ot->modal = text_scroll_modal;
ot->cancel = text_scroll_cancel;
ot->poll = text_region_scroll_poll;
/* flags */
ot->flag = OPTYPE_BLOCKING | OPTYPE_INTERNAL;
/* properties */
PropertyRNA *prop;
prop = RNA_def_int(
ot->srna, "lines", 1, INT_MIN, INT_MAX, "Lines", "Number of lines to scroll", -100, 100);
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_TEXT);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Set Selection Operator
* \{ */
struct SetSelection {
int selc, sell;
short mval_prev[2];
wmTimer *timer; /* needed for scrolling when mouse at region bounds */
};
static int flatten_width(SpaceText *st, const char *str)
{
int total = 0;
for (int i = 0; str[i]; i += BLI_str_utf8_size_safe(str + i)) {
if (str[i] == '\t') {
total += st->tabnumber - total % st->tabnumber;
}
else {
total += BLI_str_utf8_char_width_safe(str + i);
}
}
return total;
}
static int flatten_column_to_offset(SpaceText *st, const char *str, int index)
{
int i = 0, j = 0, col;
while (*(str + j)) {
if (str[j] == '\t') {
col = st->tabnumber - i % st->tabnumber;
}
else {
col = BLI_str_utf8_char_width_safe(str + j);
}
if (i + col > index) {
break;
}
i += col;
j += BLI_str_utf8_size_safe(str + j);
}
return j;
}
static TextLine *get_line_pos_wrapped(SpaceText *st, ARegion *region, int *y)
{
TextLine *linep = static_cast<TextLine *>(st->text->lines.first);
int i, lines;
if (*y < -st->top) {
return nullptr; /* We are beyond the first line... */
}
for (i = -st->top; i <= *y && linep; linep = linep->next, i += lines) {
lines = text_get_visible_lines(st, region, linep->line);
if (i + lines > *y) {
/* We found the line matching given vertical 'coordinate',
* now set y relative to this line's start. */
*y -= i;
break;
}
}
return linep;
}
static void text_cursor_set_to_pos_wrapped(
SpaceText *st, ARegion *region, int x, int y, const bool sel)
{
Text *text = st->text;
int max = wrap_width(st, region); /* column */
int charp = -1; /* mem */
bool found = false; /* flags */
/* Point to line matching given y position, if any. */
TextLine *linep = get_line_pos_wrapped(st, region, &y);
if (linep) {
int i = 0, start = 0, end = max; /* column */
int j, curs = 0, endj = 0; /* mem */
bool chop = true; /* flags */
char ch;
for (j = 0; !found && ((ch = linep->line[j]) != '\0');
j += BLI_str_utf8_size_safe(linep->line + j))
{
int chars;
int columns = BLI_str_utf8_char_width_safe(linep->line + j); /* = 1 for tab */
/* Mimic replacement of tabs */
if (ch == '\t') {
chars = st->tabnumber - i % st->tabnumber;
ch = ' ';
}
else {
chars = 1;
}
while (chars--) {
/* Gone too far, go back to last wrap point */
if (y < 0) {
charp = endj;
y = 0;
found = true;
break;
/* Exactly at the cursor */
}
if (y == 0 && i - start <= x && i + columns - start > x) {
/* current position could be wrapped to next line */
/* this should be checked when end of current line would be reached */
charp = curs = j;
found = true;
/* Prepare curs for next wrap */
}
else if (i - end <= x && i + columns - end > x) {
curs = j;
}
if (i + columns - start > max) {
end = MIN2(end, i);
if (found) {
/* exact cursor position was found, check if it's still on needed line
* (hasn't been wrapped) */
if (charp > endj && !chop && ch != '\0') {
charp = endj;
}
break;
}
if (chop) {
endj = j;
}
start = end;
end += max;
if (j < linep->len) {
y--;
}
chop = true;
if (y == 0 && i + columns - start > x) {
charp = curs;
found = true;
break;
}
}
else if (ELEM(ch, ' ', '-', '\0')) {
if (found) {
break;
}
if (y == 0 && i + columns - start > x) {
charp = curs;
found = true;
break;
}
end = i + 1;
endj = j;
chop = false;
}
i += columns;
}
}
BLI_assert(y == 0);
if (!found) {
/* On correct line but didn't meet cursor, must be at end */
charp = linep->len;
}
}
else if (y < 0) { /* Before start of text. */
linep = static_cast<TextLine *>(st->text->lines.first);
charp = 0;
}
else { /* Beyond end of text */
linep = static_cast<TextLine *>(st->text->lines.last);
charp = linep->len;
}
BLI_assert(linep && charp != -1);
if (sel) {
text->sell = linep;
text->selc = charp;
}
else {
text->curl = linep;
text->curc = charp;
}
}
static void text_cursor_set_to_pos(SpaceText *st, ARegion *region, int x, int y, const bool sel)
{
Text *text = st->text;
text_update_character_width(st);
y = (region->winy - 2 - y) / TXT_LINE_HEIGHT(st);
x -= TXT_BODY_LEFT(st);
if (x < 0) {
x = 0;
}
x = text_pixel_x_to_column(st, x) + st->left;
if (st->wordwrap) {
text_cursor_set_to_pos_wrapped(st, region, x, y, sel);
}
else {
TextLine **linep;
int *charp;
int w;
if (sel) {
linep = &text->sell;
charp = &text->selc;
}
else {
linep = &text->curl;
charp = &text->curc;
}
y -= txt_get_span(static_cast<TextLine *>(text->lines.first), *linep) - st->top;
if (y > 0) {
while (y-- != 0) {
if ((*linep)->next) {
*linep = (*linep)->next;
}
}
}
else if (y < 0) {
while (y++ != 0) {
if ((*linep)->prev) {
*linep = (*linep)->prev;
}
}
}
w = flatten_width(st, (*linep)->line);
if (x < w) {
*charp = flatten_column_to_offset(st, (*linep)->line, x);
}
else {
*charp = (*linep)->len;
}
}
if (!sel) {
txt_pop_sel(text);
}
}
static void text_cursor_timer_ensure(bContext *C, SetSelection *ssel)
{
if (ssel->timer == nullptr) {
wmWindowManager *wm = CTX_wm_manager(C);
wmWindow *win = CTX_wm_window(C);
ssel->timer = WM_event_timer_add(wm, win, TIMER, 0.02f);
}
}
static void text_cursor_timer_remove(bContext *C, SetSelection *ssel)
{
if (ssel->timer) {
wmWindowManager *wm = CTX_wm_manager(C);
wmWindow *win = CTX_wm_window(C);
WM_event_timer_remove(wm, win, ssel->timer);
}
ssel->timer = nullptr;
}
static void text_cursor_set_apply(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
SetSelection *ssel = static_cast<SetSelection *>(op->customdata);
if (event->mval[1] < 0 || event->mval[1] > region->winy) {
text_cursor_timer_ensure(C, ssel);
if (event->type == TIMER) {
text_cursor_set_to_pos(st, region, event->mval[0], event->mval[1], true);
ED_text_scroll_to_cursor(st, region, false);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
}
}
else if (!st->wordwrap && (event->mval[0] < 0 || event->mval[0] > region->winx)) {
text_cursor_timer_ensure(C, ssel);
if (event->type == TIMER) {
text_cursor_set_to_pos(
st, region, CLAMPIS(event->mval[0], 0, region->winx), event->mval[1], true);
ED_text_scroll_to_cursor(st, region, false);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
}
}
else {
text_cursor_timer_remove(C, ssel);
if (event->type != TIMER) {
text_cursor_set_to_pos(st, region, event->mval[0], event->mval[1], true);
ED_text_scroll_to_cursor(st, region, false);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
ssel->mval_prev[0] = event->mval[0];
ssel->mval_prev[1] = event->mval[1];
}
}
}
static void text_cursor_set_exit(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
SetSelection *ssel = static_cast<SetSelection *>(op->customdata);
text_update_cursor_moved(C);
text_select_update_primary_clipboard(st->text);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
text_cursor_timer_remove(C, ssel);
MEM_freeN(ssel);
}
static int text_selection_set_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
SetSelection *ssel;
if (event->mval[0] >= st->runtime.scroll_region_handle.xmin) {
return OPERATOR_PASS_THROUGH;
}
op->customdata = MEM_callocN(sizeof(SetSelection), "SetCursor");
ssel = static_cast<SetSelection *>(op->customdata);
ssel->mval_prev[0] = event->mval[0];
ssel->mval_prev[1] = event->mval[1];
ssel->sell = txt_get_span(static_cast<TextLine *>(st->text->lines.first), st->text->sell);
ssel->selc = st->text->selc;
WM_event_add_modal_handler(C, op);
text_cursor_set_apply(C, op, event);
return OPERATOR_RUNNING_MODAL;
}
static int text_selection_set_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
switch (event->type) {
case LEFTMOUSE:
case MIDDLEMOUSE:
case RIGHTMOUSE:
text_cursor_set_exit(C, op);
return OPERATOR_FINISHED;
case TIMER:
case MOUSEMOVE:
text_cursor_set_apply(C, op, event);
break;
}
return OPERATOR_RUNNING_MODAL;
}
static void text_selection_set_cancel(bContext *C, wmOperator *op)
{
text_cursor_set_exit(C, op);
}
void TEXT_OT_selection_set(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Set Selection";
ot->idname = "TEXT_OT_selection_set";
ot->description = "Set text selection";
/* api callbacks */
ot->invoke = text_selection_set_invoke;
ot->modal = text_selection_set_modal;
ot->cancel = text_selection_set_cancel;
ot->poll = text_region_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Set Cursor Operator
* \{ */
static int text_cursor_set_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
ARegion *region = CTX_wm_region(C);
int x = RNA_int_get(op->ptr, "x");
int y = RNA_int_get(op->ptr, "y");
text_cursor_set_to_pos(st, region, x, y, false);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
return OPERATOR_PASS_THROUGH;
}
static int text_cursor_set_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
if (event->mval[0] >= st->runtime.scroll_region_handle.xmin) {
return OPERATOR_PASS_THROUGH;
}
RNA_int_set(op->ptr, "x", event->mval[0]);
RNA_int_set(op->ptr, "y", event->mval[1]);
return text_cursor_set_exec(C, op);
}
void TEXT_OT_cursor_set(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Set Cursor";
ot->idname = "TEXT_OT_cursor_set";
ot->description = "Set cursor position";
/* api callbacks */
ot->invoke = text_cursor_set_invoke;
ot->exec = text_cursor_set_exec;
ot->poll = text_region_edit_poll;
/* properties */
RNA_def_int(ot->srna, "x", 0, INT_MIN, INT_MAX, "X", "", INT_MIN, INT_MAX);
RNA_def_int(ot->srna, "y", 0, INT_MIN, INT_MAX, "Y", "", INT_MIN, INT_MAX);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Line Number Operator
* \{ */
static int text_line_number_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
ARegion *region = CTX_wm_region(C);
const int *mval = event->mval;
double time;
static int jump_to = 0;
static double last_jump = 0;
text_update_character_width(st);
if (!st->showlinenrs) {
return OPERATOR_PASS_THROUGH;
}
if (!(mval[0] > 2 &&
mval[0] < (TXT_NUMCOL_WIDTH(st) + (TXT_BODY_LPAD * st->runtime.cwidth_px)) &&
mval[1] > 2 && mval[1] < region->winy - 2))
{
return OPERATOR_PASS_THROUGH;
}
const char event_ascii = WM_event_utf8_to_ascii(event);
if (!(event_ascii >= '0' && event_ascii <= '9')) {
return OPERATOR_PASS_THROUGH;
}
time = PIL_check_seconds_timer();
if (last_jump < time - 1) {
jump_to = 0;
}
jump_to *= 10;
jump_to += int(event_ascii - '0');
txt_move_toline(text, jump_to - 1, false);
last_jump = time;
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
return OPERATOR_FINISHED;
}
void TEXT_OT_line_number(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Line Number";
ot->idname = "TEXT_OT_line_number";
ot->description = "The current line number";
/* api callbacks */
ot->invoke = text_line_number_invoke;
ot->poll = text_region_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Insert Operator
* \{ */
static int text_insert_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
char *str;
int str_len;
bool done = false;
size_t i = 0;
uint code;
text_drawcache_tag_update(st, false);
str = RNA_string_get_alloc(op->ptr, "text", nullptr, 0, &str_len);
ED_text_undo_push_init(C);
if (st && st->overwrite) {
while (str[i]) {
code = BLI_str_utf8_as_unicode_step_safe(str, str_len, &i);
done |= txt_replace_char(text, code);
}
}
else {
while (str[i]) {
code = BLI_str_utf8_as_unicode_step_safe(str, str_len, &i);
done |= txt_add_char(text, code);
}
}
MEM_freeN(str);
if (!done) {
return OPERATOR_CANCELLED;
}
text_update_line_edited(text->curl);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
return OPERATOR_FINISHED;
}
static int text_insert_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
SpaceText *st = CTX_wm_space_text(C);
int ret;
/* Auto-close variables. */
bool do_auto_close = false;
bool do_auto_close_select = false;
uint auto_close_char_input = 0;
uint auto_close_char_match = 0;
/* Variables needed to restore the selection when auto-closing around an existing selection. */
struct {
TextLine *sell;
TextLine *curl;
int selc;
int curc;
} auto_close_select = {nullptr}, auto_close_select_backup = {nullptr};
/* NOTE: the "text" property is always set from key-map,
* so we can't use #RNA_struct_property_is_set, check the length instead. */
if (!RNA_string_length(op->ptr, "text")) {
/* If Alt/Control/Super are pressed pass through except for utf8 character event
* (when input method are used for utf8 inputs, the user may assign key event
* including Alt/Control/Super like Control-M to commit utf8 string.
* In such case, the modifiers in the utf8 character event make no sense). */
if ((event->modifier & (KM_CTRL | KM_OSKEY)) && !event->utf8_buf[0]) {
return OPERATOR_PASS_THROUGH;
}
char str[BLI_UTF8_MAX + 1];
const size_t len = BLI_str_utf8_size_safe(event->utf8_buf);
memcpy(str, event->utf8_buf, len);
str[len] = '\0';
RNA_string_set(op->ptr, "text", str);
if (U.text_flag & USER_TEXT_EDIT_AUTO_CLOSE) {
auto_close_char_input = BLI_str_utf8_as_unicode_or_error(str);
if (isascii(auto_close_char_input)) {
auto_close_char_match = text_closing_character_pair_get(auto_close_char_input);
if (auto_close_char_match != 0) {
do_auto_close = true;
if (txt_has_sel(st->text) &&
!text_span_is_blank(st->text->sell, st->text->selc, st->text->curl, st->text->curc))
{
do_auto_close_select = true;
auto_close_select_backup.curl = st->text->curl;
auto_close_select_backup.curc = st->text->curc;
auto_close_select_backup.sell = st->text->sell;
auto_close_select_backup.selc = st->text->selc;
/* Movers the cursor to the start of the selection. */
txt_order_cursors(st->text, false);
auto_close_select.curl = st->text->curl;
auto_close_select.curc = st->text->curc;
auto_close_select.sell = st->text->sell;
auto_close_select.selc = st->text->selc;
txt_pop_sel(st->text);
}
}
}
}
}
ret = text_insert_exec(C, op);
if (do_auto_close) {
if (ret == OPERATOR_FINISHED) {
const int auto_close_char_len = BLI_str_utf8_from_unicode_len(auto_close_char_input);
/* If there was a selection, move cursor to the end of it. */
if (do_auto_close_select) {
/* Update the value in-place as needed. */
if (auto_close_select.curl == auto_close_select.sell) {
auto_close_select.selc += auto_close_char_len;
}
/* Move the cursor to the end of the selection. */
st->text->curl = auto_close_select.sell;
st->text->curc = auto_close_select.selc;
txt_pop_sel(st->text);
}
txt_add_char(st->text, auto_close_char_match);
txt_move_left(st->text, false);
/* If there was a selection, restore it. */
if (do_auto_close_select) {
/* Mark the selection as edited. */
if (auto_close_select.curl != auto_close_select.sell) {
TextLine *line = auto_close_select.curl;
do {
line = line->next;
text_update_line_edited(line);
} while (line != auto_close_select.sell);
}
st->text->curl = auto_close_select.curl;
st->text->curc = auto_close_select.curc + auto_close_char_len;
st->text->sell = auto_close_select.sell;
st->text->selc = auto_close_select.selc;
}
}
else {
/* If nothing was done & the selection was removed, restore the selection. */
if (do_auto_close_select) {
st->text->curl = auto_close_select_backup.curl;
st->text->curc = auto_close_select_backup.curc;
st->text->sell = auto_close_select_backup.sell;
st->text->selc = auto_close_select_backup.selc;
}
}
}
/* run the script while editing, evil but useful */
if (ret == OPERATOR_FINISHED && st->live_edit) {
text_run_script(C, nullptr);
}
return ret;
}
void TEXT_OT_insert(wmOperatorType *ot)
{
PropertyRNA *prop;
/* identifiers */
ot->name = "Insert";
ot->idname = "TEXT_OT_insert";
ot->description = "Insert text at cursor position";
/* api callbacks */
ot->exec = text_insert_exec;
ot->invoke = text_insert_invoke;
ot->poll = text_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
prop = RNA_def_string(
ot->srna, "text", nullptr, 0, "Text", "Text to insert at the cursor position");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Find Operator
* \{ */
/* mode */
enum {
TEXT_FIND = 0,
TEXT_REPLACE = 1,
};
static int text_find_and_replace(bContext *C, wmOperator *op, short mode)
{
Main *bmain = CTX_data_main(C);
SpaceText *st = CTX_wm_space_text(C);
Text *text = st->text;
int flags;
int found = 0;
char *tmp;
if (!st->findstr[0]) {
return OPERATOR_CANCELLED;
}
flags = st->flags;
if (flags & ST_FIND_ALL) {
flags &= ~ST_FIND_WRAP;
}
/* Replace current */
if (mode != TEXT_FIND && txt_has_sel(text)) {
tmp = txt_sel_to_buf(text, nullptr);
if (flags & ST_MATCH_CASE) {
found = STREQ(st->findstr, tmp);
}
else {
found = BLI_strcasecmp(st->findstr, tmp) == 0;
}
if (found) {
if (mode == TEXT_REPLACE) {
ED_text_undo_push_init(C);
txt_insert_buf(text, st->replacestr, strlen(st->replacestr));
if (text->curl && text->curl->format) {
MEM_freeN(text->curl->format);
text->curl->format = nullptr;
}
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
text_drawcache_tag_update(st, true);
}
}
MEM_freeN(tmp);
tmp = nullptr;
}
/* Find next */
if (txt_find_string(text, st->findstr, flags & ST_FIND_WRAP, flags & ST_MATCH_CASE)) {
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
}
else if (flags & ST_FIND_ALL) {
if (text->id.next) {
text = st->text = static_cast<Text *>(text->id.next);
}
else {
text = st->text = static_cast<Text *>(bmain->texts.first);
}
txt_move_toline(text, 0, false);
text_update_cursor_moved(C);
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
}
else {
if (!found) {
BKE_reportf(op->reports, RPT_WARNING, "Text not found: %s", st->findstr);
}
}
return OPERATOR_FINISHED;
}
static int text_find_exec(bContext *C, wmOperator *op)
{
return text_find_and_replace(C, op, TEXT_FIND);
}
void TEXT_OT_find(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Find Next";
ot->idname = "TEXT_OT_find";
ot->description = "Find specified text";
/* api callbacks */
ot->exec = text_find_exec;
ot->poll = text_space_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Replace Operator
* \{ */
static int text_replace_all(bContext *C)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = st->text;
const int flags = st->flags;
int found = 0;
if (!st->findstr[0]) {
return OPERATOR_CANCELLED;
}
const int orig_curl = BLI_findindex(&text->lines, text->curl);
const int orig_curc = text->curc;
bool has_sel = txt_has_sel(text);
txt_move_toline(text, 0, false);
found = txt_find_string(text, st->findstr, 0, flags & ST_MATCH_CASE);
if (found) {
ED_text_undo_push_init(C);
do {
txt_insert_buf(text, st->replacestr, strlen(st->replacestr));
if (text->curl && text->curl->format) {
MEM_freeN(text->curl->format);
text->curl->format = nullptr;
}
found = txt_find_string(text, st->findstr, 0, flags & ST_MATCH_CASE);
} while (found);
WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
text_drawcache_tag_update(st, true);
}
else {
/* Restore position */
txt_move_to(text, orig_curl, orig_curc, has_sel);
return OPERATOR_CANCELLED;
}
return OPERATOR_FINISHED;
}
static int text_replace_exec(bContext *C, wmOperator *op)
{
bool replace_all = RNA_boolean_get(op->ptr, "all");
if (replace_all) {
return text_replace_all(C);
}
return text_find_and_replace(C, op, TEXT_REPLACE);
}
void TEXT_OT_replace(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Replace";
ot->idname = "TEXT_OT_replace";
ot->description = "Replace text with the specified text";
/* api callbacks */
ot->exec = text_replace_exec;
ot->poll = text_space_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
PropertyRNA *prop;
prop = RNA_def_boolean(ot->srna, "all", false, "Replace All", "Replace all occurrences");
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Find Set Selected
* \{ */
static int text_find_set_selected_exec(bContext *C, wmOperator *op)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
char *tmp;
tmp = txt_sel_to_buf(text, nullptr);
STRNCPY(st->findstr, tmp);
MEM_freeN(tmp);
if (!st->findstr[0]) {
return OPERATOR_FINISHED;
}
return text_find_and_replace(C, op, TEXT_FIND);
}
void TEXT_OT_find_set_selected(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Find & Set Selection";
ot->idname = "TEXT_OT_find_set_selected";
ot->description = "Find specified text and set as selected";
/* api callbacks */
ot->exec = text_find_set_selected_exec;
ot->poll = text_space_edit_poll;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Replace Set Selected
* \{ */
static int text_replace_set_selected_exec(bContext *C, wmOperator * /*op*/)
{
SpaceText *st = CTX_wm_space_text(C);
Text *text = CTX_data_edit_text(C);
char *tmp;
tmp = txt_sel_to_buf(text, nullptr);
STRNCPY(st->replacestr, tmp);
MEM_freeN(tmp);
return OPERATOR_FINISHED;
}
void TEXT_OT_replace_set_selected(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Replace & Set Selection";
ot->idname = "TEXT_OT_replace_set_selected";
ot->description = "Replace text with specified text and set as selected";
/* api callbacks */
ot->exec = text_replace_set_selected_exec;
ot->poll = text_space_edit_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Jump to File at Point
* \{ */
static bool text_jump_to_file_at_point_external(bContext *C,
ReportList *reports,
const char *filepath,
const int line_index,
const int column_index)
{
bool success = false;
#ifdef WITH_PYTHON
BPy_RunErrInfo err_info = {};
err_info.reports = reports;
err_info.report_prefix = "External editor";
const char *expr_imports[] = {"bl_text_utils", "bl_text_utils.external_editor", "os", nullptr};
std::string expr;
{
std::stringstream expr_stream;
expr_stream << "bl_text_utils.external_editor.open_external_editor(os.fsdecode(b'";
for (const char *ch = filepath; *ch; ch++) {
expr_stream << "\\x" << std::hex << int(*ch);
}
expr_stream << "'), " << std::dec << line_index << ", " << std::dec << column_index << ")";
expr = expr_stream.str();
}
char *expr_result = nullptr;
if (BPY_run_string_as_string(C, expr_imports, expr.c_str(), &err_info, &expr_result)) {
/* No error. */
if (expr_result[0] == '\0') {
BKE_reportf(
reports, RPT_INFO, "See '%s' in the external editor", BLI_path_basename(filepath));
success = true;
}
else {
BKE_report(reports, RPT_ERROR, expr_result);
}
MEM_freeN(expr_result);
}
#else
UNUSED_VARS(C, reports, filepath, line_index, column_index);
#endif /* WITH_PYTHON */
return success;
}
static bool text_jump_to_file_at_point_internal(bContext *C,
ReportList *reports,
const char *filepath,
const int line_index,
const int column_index)
{
Main *bmain = CTX_data_main(C);
Text *text = nullptr;
LISTBASE_FOREACH (Text *, text_iter, &bmain->texts) {
if (text_iter->filepath && BLI_path_cmp(text_iter->filepath, filepath) == 0) {
text = text_iter;
break;
}
}
if (text == nullptr) {
text = BKE_text_load(bmain, filepath, BKE_main_blendfile_path(bmain));
}
if (text == nullptr) {
BKE_reportf(reports, RPT_WARNING, "File '%s' cannot be opened", filepath);
return false;
}
txt_move_to(text, line_index, column_index, false);
/* naughty!, find text area to set, not good behavior
* but since this is a developer tool lets allow it - campbell */
if (!ED_text_activate_in_screen(C, text)) {
BKE_reportf(reports, RPT_INFO, "See '%s' in the text editor", text->id.name + 2);
}
WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
return true;
}
static int text_jump_to_file_at_point_exec(bContext *C, wmOperator *op)
{
PropertyRNA *prop_filepath = RNA_struct_find_property(op->ptr, "filepath");
PropertyRNA *prop_line = RNA_struct_find_property(op->ptr, "line");
PropertyRNA *prop_column = RNA_struct_find_property(op->ptr, "column");
if (!RNA_property_is_set(op->ptr, prop_filepath)) {
if (const Text *text = CTX_data_edit_text(C)) {
if (text->filepath != nullptr) {
const TextLine *line = text->curl;
const int line_index = BLI_findindex(&text->lines, text->curl);
const int column_index = BLI_str_utf8_offset_to_index(line->line, line->len, text->curc);
RNA_property_string_set(op->ptr, prop_filepath, text->filepath);
RNA_property_int_set(op->ptr, prop_line, line_index);
RNA_property_int_set(op->ptr, prop_column, column_index);
}
}
}
char filepath[FILE_MAX];
RNA_property_string_get(op->ptr, prop_filepath, filepath);
const int line_index = RNA_property_int_get(op->ptr, prop_line);
const int column_index = RNA_property_int_get(op->ptr, prop_column);
if (filepath[0] == '\0') {
BKE_report(op->reports, RPT_WARNING, "File path property not set");
return OPERATOR_CANCELLED;
}
bool success;
if (U.text_editor[0] != '\0') {
success = text_jump_to_file_at_point_external(
C, op->reports, filepath, line_index, column_index);
}
else {
success = text_jump_to_file_at_point_internal(
C, op->reports, filepath, line_index, column_index);
}
return success ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
void TEXT_OT_jump_to_file_at_point(wmOperatorType *ot)
{
PropertyRNA *prop;
/* identifiers */
ot->name = "Jump to File at Point";
ot->idname = "TEXT_OT_jump_to_file_at_point";
ot->description = "Jump to a file for the text editor";
/* api callbacks */
ot->exec = text_jump_to_file_at_point_exec;
/* flags */
ot->flag = 0;
prop = RNA_def_string(ot->srna, "filepath", nullptr, FILE_MAX, "Filepath", "");
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
prop = RNA_def_int(ot->srna, "line", 0, 0, INT_MAX, "Line", "Line to jump to", 1, 10000);
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
prop = RNA_def_int(ot->srna, "column", 0, 0, INT_MAX, "Column", "Column to jump to", 1, 10000);
RNA_def_property_flag(prop, PropertyFlag(PROP_HIDDEN | PROP_SKIP_SAVE));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Resolve Conflict Operator
* \{ */
enum { RESOLVE_IGNORE, RESOLVE_RELOAD, RESOLVE_SAVE, RESOLVE_MAKE_INTERNAL };
static const EnumPropertyItem resolution_items[] = {
{RESOLVE_IGNORE, "IGNORE", 0, "Ignore", ""},
{RESOLVE_RELOAD, "RELOAD", 0, "Reload", ""},
{RESOLVE_SAVE, "SAVE", 0, "Save", ""},
{RESOLVE_MAKE_INTERNAL, "MAKE_INTERNAL", 0, "Make Internal", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static bool text_resolve_conflict_poll(bContext *C)
{
Text *text = CTX_data_edit_text(C);
if (!text_edit_poll(C)) {
return false;
}
return ((text->filepath != nullptr) && !(text->flags & TXT_ISMEM));
}
static int text_resolve_conflict_exec(bContext *C, wmOperator *op)
{
Text *text = CTX_data_edit_text(C);
int resolution = RNA_enum_get(op->ptr, "resolution");
switch (resolution) {
case RESOLVE_RELOAD:
return text_reload_exec(C, op);
case RESOLVE_SAVE:
return text_save_exec(C, op);
case RESOLVE_MAKE_INTERNAL:
return text_make_internal_exec(C, op);
case RESOLVE_IGNORE:
BKE_text_file_modified_ignore(text);
return OPERATOR_FINISHED;
}
return OPERATOR_CANCELLED;
}
static int text_resolve_conflict_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
Text *text = CTX_data_edit_text(C);
uiPopupMenu *pup;
uiLayout *layout;
switch (BKE_text_file_modified_check(text)) {
case 1:
if (text->flags & TXT_ISDIRTY) {
/* Modified locally and externally, ah. offer more possibilities. */
pup = UI_popup_menu_begin(
C, IFACE_("File Modified Outside and Inside Blender"), ICON_NONE);
layout = UI_popup_menu_layout(pup);
uiItemEnumO_ptr(layout,
op->type,
IFACE_("Reload from disk (ignore local changes)"),
ICON_NONE,
"resolution",
RESOLVE_RELOAD);
uiItemEnumO_ptr(layout,
op->type,
IFACE_("Save to disk (ignore outside changes)"),
ICON_NONE,
"resolution",
RESOLVE_SAVE);
uiItemEnumO_ptr(layout,
op->type,
IFACE_("Make text internal (separate copy)"),
ICON_NONE,
"resolution",
RESOLVE_MAKE_INTERNAL);
UI_popup_menu_end(C, pup);
}
else {
pup = UI_popup_menu_begin(C, IFACE_("File Modified Outside Blender"), ICON_NONE);
layout = UI_popup_menu_layout(pup);
uiItemEnumO_ptr(
layout, op->type, IFACE_("Reload from disk"), ICON_NONE, "resolution", RESOLVE_RELOAD);
uiItemEnumO_ptr(layout,
op->type,
IFACE_("Make text internal (separate copy)"),
ICON_NONE,
"resolution",
RESOLVE_MAKE_INTERNAL);
uiItemEnumO_ptr(
layout, op->type, IFACE_("Ignore"), ICON_NONE, "resolution", RESOLVE_IGNORE);
UI_popup_menu_end(C, pup);
}
break;
case 2:
pup = UI_popup_menu_begin(C, IFACE_("File Deleted Outside Blender"), ICON_NONE);
layout = UI_popup_menu_layout(pup);
uiItemEnumO_ptr(layout,
op->type,
IFACE_("Make text internal"),
ICON_NONE,
"resolution",
RESOLVE_MAKE_INTERNAL);
uiItemEnumO_ptr(
layout, op->type, IFACE_("Recreate file"), ICON_NONE, "resolution", RESOLVE_SAVE);
UI_popup_menu_end(C, pup);
break;
}
return OPERATOR_INTERFACE;
}
void TEXT_OT_resolve_conflict(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Resolve Conflict";
ot->idname = "TEXT_OT_resolve_conflict";
ot->description = "When external text is out of sync, resolve the conflict";
/* api callbacks */
ot->exec = text_resolve_conflict_exec;
ot->invoke = text_resolve_conflict_invoke;
ot->poll = text_resolve_conflict_poll;
/* properties */
RNA_def_enum(ot->srna,
"resolution",
resolution_items,
RESOLVE_IGNORE,
"Resolution",
"How to solve conflict due to differences in internal and external text");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name To 3D Object Operator
* \{ */
static int text_to_3d_object_exec(bContext *C, wmOperator *op)
{
const Text *text = CTX_data_edit_text(C);
const bool split_lines = RNA_boolean_get(op->ptr, "split_lines");
ED_text_to_object(C, text, split_lines);
return OPERATOR_FINISHED;
}
void TEXT_OT_to_3d_object(wmOperatorType *ot)
{
/* identifiers */
ot->name = "To 3D Object";
ot->idname = "TEXT_OT_to_3d_object";
ot->description = "Create 3D text object from active text data-block";
/* api callbacks */
ot->exec = text_to_3d_object_exec;
ot->poll = text_data_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* properties */
RNA_def_boolean(
ot->srna, "split_lines", false, "Split Lines", "Create one object per line in the text");
}
/** \} */