1 #include "tmeditorwidget.h"
12 #include <QDesktopServices>
14 #include <QVBoxLayout>
17 #include <QMutexLocker>
20 #include <QTextDocument>
21 #include <QTextCursor>
23 #include <QTextBlockFormat>
35 TM::EditorWidget::EditorWidget(Settings *settings, Service *service, QWidget *parent)
37 , m_mutex(QMutex::Recursive)
39 , m_settings(settings)
43 QVBoxLayout *vlayout = new QVBoxLayout(this);
44 vlayout->setSpacing(4);
45 vlayout->setContentsMargins(0, 0, 0, 0);
47 m_toolbar = new QToolBar("Editor", this);
49 m_edit_mode = new QAction(QIcon(":/edit.png"), "edit", this);
50 m_edit_mode->setCheckable(true);
51 m_toolbar->addAction(m_edit_mode);
52 connect(m_edit_mode, SIGNAL(triggered(bool)), this, SLOT(onEditModeTriggered(bool)));
54 m_link = new QAction(QIcon(":/link.png"), "link", this);
55 m_link->setCheckable(true);
56 m_link->setDisabled(true);
57 m_toolbar->addAction(m_link);
58 connect(m_link, SIGNAL(triggered(bool)), this, SLOT(onLinkModeTriggered(bool)));
60 m_slang = new QAction("&Source", this);
61 m_slang->setDisabled(true);
62 m_slang->setMenu(new QMenu(this));
63 m_tlang = new QAction("&Target", this);
64 m_tlang->setDisabled(true);
65 m_tlang->setMenu(new QMenu(this));
66 m_toolbar->addAction(m_slang);
67 m_toolbar->addAction(m_tlang);
69 QAction *browser = new QAction(QIcon(":/browser.png"), "browser", this);
70 m_toolbar->addAction(browser);
71 connect(browser, SIGNAL(triggered(bool)), this, SLOT(onBrowserTriggered(bool)));
73 QAction *setting = new QAction(QIcon(":/setting.png"), "setting", this);
74 m_toolbar->addAction(setting);
76 m_edit = new Editor(settings, service, this);
77 vlayout->addWidget(m_toolbar);
78 vlayout->addWidget(m_edit);
80 connect(m_service, SIGNAL(languageLoaded(int, QString, QIcon)), this, SLOT(onLanguageLoaded(int,QString,QIcon)));
84 * \brief ブラウザ上でドキュメントがフォーカスを取得したとき、
85 * SocketConnectionから呼び出されます。
87 * SocketConnectionは別スレッドに在ります。
89 void TM::EditorWidget::attach(SocketConnection *socket)
91 if(m_socket == socket) return;
92 if(m_socket) detach(m_socket);
94 QMutexLocker lock(&m_mutex);
96 connect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
97 set_link_mode_disabled(true);
101 * \brief ブラウザ上でドキュメントがフォーカスを取得することで、
102 * 現在のSocketConnectionがフォーカスを失った時に呼び出されます。
104 * このメンバはattach()から呼び出されますが、その呼び出し元は別スレッドに在ります。
106 void TM::EditorWidget::detach(SocketConnection *)
108 QMutexLocker lock(&m_mutex);
109 disconnect(this, SIGNAL(editModeChanged(bool)), m_socket, SLOT(changeEditMode(bool)));
115 void TM::EditorWidget::set_http_port(quint16 http_port) { m_http_port = http_port; }
118 * \brief 編集モードを変更します。
120 void TM::EditorWidget::set_edit_mode(bool mode)
122 QMutexLocker lock(&m_mutex);
123 bool old_mode = m_edit_mode->isChecked();
124 if(old_mode == mode) return;
126 m_edit_mode->setChecked(mode);
130 * \brief 編集モードの場合trueを返します。
132 bool TM::EditorWidget::edit_mode()
134 QMutexLocker lock(&m_mutex);
135 return m_edit_mode->isChecked();
139 * \brief リンクモードを設定します。
141 void TM::EditorWidget::set_link_mode(bool mode)
143 QMutexLocker lock(&m_mutex);
144 bool old_mode = m_link->isChecked();
145 if(old_mode == mode) return;
147 m_link->setChecked(mode);
151 * \brief リンクモードの場合、trueを返します。
153 bool TM::EditorWidget::link_mode()
155 QMutexLocker lock(&m_mutex);
156 return m_link->isChecked();
160 * \brief リンクボタンをグレーアウトします。
162 void TM::EditorWidget::set_link_mode_disabled(bool disable)
164 QMutexLocker lock(&m_mutex);
165 m_link->setDisabled(disable);
169 * \brief 原文の言語コードを返します。
171 int TM::EditorWidget::source_language()
173 QMutexLocker lock(&m_mutex);
174 return m_slang->data().toInt();
178 * \brief 訳文の言語コードを返します。
180 int TM::EditorWidget::target_language()
182 QMutexLocker lock(&m_mutex);
183 return m_tlang->data().toInt();
186 void TM::EditorWidget::set_segment(TextSegment::pointer segment)
188 QMutexLocker lock(&m_mutex);
189 m_edit->set_segment(segment);
192 void TM::EditorWidget::save_sentence(int segment_id, int index,
193 Text::pointer target_sentence, QJsonArray link)
195 qDebug() << "save: " << link;
196 m_socket->save_sentence(segment_id, index, target_sentence, link);
200 * \brief 言語プラグインが読み込まれるたびに呼び出されます。
203 * \param icon 言語を表すアイコン。
205 void TM::EditorWidget::onLanguageLoaded(int code, QString name, QIcon icon)
207 QString dslanguage = m_settings->value("Widget/defaultSourceLanguage", "English").toString();
208 QString tslanguage = m_settings->value("Widget/defaultSourceLanguage", "Japanese").toString();
209 if(dslanguage == name)
211 m_slang->setIcon(icon);
212 m_slang->setData(code);
214 if(tslanguage == name)
216 m_tlang->setIcon(icon);
217 m_tlang->setData(code);
219 QAction *saction = m_slang->menu()->addAction(icon, name);
220 saction->setData(code);
221 connect(saction, SIGNAL(triggered(bool)), this, SLOT(onSourceLanguageTriggered(bool)));
222 QAction *taction = m_tlang->menu()->addAction(icon, name);
223 taction->setData(code);
224 connect(taction, SIGNAL(triggered(bool)), this, SLOT(onTargetLanguageTriggered(bool)));
228 * \brief 編集アイコンがクリックされたとき呼び出されます。
230 void TM::EditorWidget::onEditModeTriggered(bool)
232 QAction *edit_mode = qobject_cast<QAction*>(sender());
233 bool checked = edit_mode->isChecked();
234 m_link->setDisabled(true);
235 m_slang->setDisabled(!checked);
236 m_tlang->setDisabled(!checked);
237 emit editModeChanged(checked);
239 m_edit->set_edit_mode(checked);
243 * \brief リンクアイコンがクリックされたとき呼び出されます。
245 void TM::EditorWidget::onLinkModeTriggered(bool checked)
247 //QAction *link_mode = qobject_cast<QAction*>(sender());
248 m_edit->set_link_mode(checked);
252 * \brief アクションから原文言語が変更されたとき呼び出されます。
254 void TM::EditorWidget::onSourceLanguageTriggered(bool)
256 QAction *saction = qobject_cast<QAction*>(sender());
258 m_slang->setIcon(saction->icon());
259 m_slang->setData(saction->data());
263 * \brief アクションから訳文言語が変更されたとき呼び出されます。
265 void TM::EditorWidget::onTargetLanguageTriggered(bool)
267 QAction *taction = qobject_cast<QAction*>(sender());
269 m_tlang->setIcon(taction->icon());
270 m_tlang->setData(taction->data());
274 * \brief ブラウザ・アイコンがクリックされたとき呼び出されます。
276 void TM::EditorWidget::onBrowserTriggered(bool)
278 QString url("http://localhost:");
279 url += QString::number(m_http_port) + "/";
281 QDesktopServices::openUrl(QUrl(url));
284 // EditorPanel ----------------------------------------------------------------
286 TM::EditorPanel::EditorPanel(QWidget *parent)
293 * \brief 親となるエディタを設定します。
295 void TM::EditorPanel::set_editor(Editor *editor)
302 * \brief 保持している文を返します。
304 Text::pointer TM::EditorPanel::sentence() { return m_sentence; }
309 void TM::EditorPanel::set_sentence(Text::pointer sentence)
312 m_sentence = sentence;
314 QTextCursor c = textCursor();
315 for(Text::pointer p = sentence->begin(); p; p = p->next()) // p=word
318 QVariant v = QVariant::fromValue(Text::weak_pointer(p));
319 cf.setProperty(Word, v);
320 c.insertText(p->string(), cf);
325 * \brief 内容が空の場合、trueを返します。
327 bool TM::EditorPanel::is_empty() const { return document()->isEmpty(); }
330 * \brief 引数として与えられた単語に該当する範囲を選択するカーソルを返します。
332 QTextCursor TM::EditorPanel::select_cursor(Text::pointer word)
334 RangeData *rd = static_cast<RangeData*>(word->data().get());
336 int slength = word->string().length();
337 QTextCursor c = textCursor();
338 c.movePosition(QTextCursor::Start);
339 c.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, rd->begin());
340 c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, slength);
345 * \brief 引数として与えられた位置にある単語を返します。
347 Text::pointer TM::EditorPanel::select_word(QPoint const &pos)
349 QTextCursor c = cursorForPosition(pos);
351 QVariant v = c.charFormat().property(Word);
352 if(!v.isValid()) return Text::pointer();
353 Text::weak_pointer wp = v.value<Text::weak_pointer>();
354 assert(!wp.expired());
355 Text::pointer p = wp.lock();
360 * \brief wordの範囲に背景色colorを設定します。
362 * 透過色は、Qt::transparentです。
364 void TM::EditorPanel::highlight(Text::pointer word, QColor color)
366 QTextCursor c = select_cursor(word);
367 QTextCharFormat cf = c.charFormat();
368 cf.setBackground(color);
369 c.mergeCharFormat(cf);
372 void TM::EditorPanel::highlight(WordLink::storage_type *link, QColor color)
374 for(Text::pointer p : *link) highlight(p, color);
377 void TM::EditorPanel::clear_highlight()
379 if(m_sentence) set_sentence(m_sentence);
383 * \brief 引数として与えられた整数値から色を作成します。
385 * 色分け用に使えるQColorを返します。
387 QColor TM::EditorPanel::color(int index) const
390 result.setHsv(index * 199, 128, 128, 128);
394 // SourcePanel ----------------------------------------------------------------
396 TM::SourcePanel::SourcePanel(QWidget *parent)
397 : EditorPanel(parent)
399 , m_target_panel(nullptr)
401 setUndoRedoEnabled(false);
402 setContextMenuPolicy(Qt::NoContextMenu);
405 int TM::SourcePanel::index() const { return m_index; }
407 void TM::SourcePanel::set_index(int index) { m_index = index; }
409 TM::TargetPanel* TM::SourcePanel::target_panel() { return m_target_panel; }
411 void TM::SourcePanel::set_target_panel(TargetPanel *target)
413 m_target_panel = target;
416 void TM::SourcePanel::commit_link()
420 m_target_panel->clear_highlight();
423 TM::WordLinker* TM::SourcePanel::linker()
429 * \brief 保持するリンクを色分け表示します。
431 void TM::SourcePanel::ensure_highlight()
434 m_target_panel->clear_highlight();
436 WordLink::pointer wl = m_linker.current();
439 highlight(wl->sources(), Qt::cyan);
440 target_panel()->highlight(wl->targets(), Qt::cyan);
443 for(WordLink::pointer wl : m_linker)
445 int index = m_linker.index_of(wl);
446 QColor c = color(index);
447 highlight(wl->sources(), c);
448 target_panel()->highlight(wl->targets(), c);
452 bool TM::SourcePanel::canInsertFromMimeData(const QMimeData *) const
454 return false; // ドロップ禁止カーソルにする
457 void TM::SourcePanel::insertFromMimeData(const QMimeData *) { } // ドロップされても何もしない
459 void TM::SourcePanel::inputMethodEvent(QInputMethodEvent *ev)
461 ev->setCommitString(""); // 入力禁止
462 QPlainTextEdit::inputMethodEvent(ev);
465 void TM::SourcePanel::keyPressEvent(QKeyEvent *ev)
472 if(m_editor->link_mode()) m_editor->set_link_mode(false);
475 if(!ev->modifiers().testFlag(Qt::ControlModifier)) break;
480 TextPanel::keyPressEvent(ev);
485 void TM::SourcePanel::do_click(QPoint const &pos)
487 if(m_editor->link_mode()) do_click_in_link_mode(pos);
490 void TM::SourcePanel::do_click_in_link_mode(QPoint const &pos)
492 Text::pointer w = select_word(pos);
495 m_linker.toggle(WordLink::Source, w);
497 setTextCursor(cursorForPosition(pos));
500 void TM::SourcePanel::do_focusin()
502 //if(m_target_panel) m_target_panel->show();
505 void TM::SourcePanel::do_focusout()
509 // TargetPanel ----------------------------------------------------------------
511 TM::TargetPanel::TargetPanel(QWidget *parent)
512 : EditorPanel(parent)
513 , m_source_panel(nullptr)
514 , m_text_dirty(false)
517 //setContextMenuPolicy(Qt::NoContextMenu);
520 TM::SourcePanel* TM::TargetPanel::source_panel() { return m_source_panel; }
522 void TM::TargetPanel::set_source_panel(SourcePanel *source)
524 m_source_panel = source;
527 void TM::TargetPanel::set_sentence(Text::pointer sentence)
529 EditorPanel::set_sentence(sentence);
530 set_text_dirty(false);
531 set_text_saved(false);
535 * \brief データベースへの登録が必要な場合、falseを返します。
537 * パネル内の文字列が編集された場合、trueを返します。
538 * 原文パネルがフォーカスを失うと、エディタがこのメンバを呼び出し、
539 * データベース登録の必要性を判定します。
540 * その後、エディタはデータベース登録を行い、フラグをクリアするので、
541 * 次に文字列が編集されるまでtrueを返し続けます。
543 bool TM::TargetPanel::is_text_saved() const { return m_text_saved; }
545 void TM::TargetPanel::set_text_saved(bool saved)
547 m_text_saved = saved;
551 * \brief パネル内の文字列が編集された場合、trueを返します。
553 * エディタがリンクモードに入るとき、このメンバを呼び出し、
554 * 単語へ分割する必要があるか判定します。
555 * エディタはパネルの文字列を単語に分割した後、set_sentence()によって
556 * パネルに文を設定するため、次に文字列を編集するまでfalseを返すようになります。
558 bool TM::TargetPanel::is_text_dirty() const { return m_text_dirty; }
561 * \brief パネルの文字列が編集されたかを示すフラグを設定します。
563 void TM::TargetPanel::set_text_dirty(bool dirty)
565 m_text_dirty = dirty;
566 if(dirty) m_editor->parent_editor_widget()->set_link_mode_disabled(false);
569 bool TM::TargetPanel::canInsertFromMimeData(QMimeData const *source) const
571 if(source->hasText()) return true;
575 void TM::TargetPanel::insertFromMimeData(QMimeData const *source)
577 TextPanel::insertFromMimeData(source);
578 set_text_dirty(true);
579 set_text_saved(false);
582 void TM::TargetPanel::inputMethodEvent(QInputMethodEvent *ev)
584 if(m_editor->link_mode()) ev->setCommitString("");
585 if(!ev->commitString().isEmpty())
587 set_text_dirty(true);
588 set_text_saved(false);
590 QPlainTextEdit::inputMethodEvent(ev);
593 void TM::TargetPanel::keyPressEvent(QKeyEvent *ev)
595 if(m_editor->link_mode()) do_key_press_in_link_mode(ev);
598 TextPanel::keyPressEvent(ev);
599 if(!ev->text().isEmpty())
601 set_text_dirty(true);
602 set_text_saved(false);
607 void TM::TargetPanel::do_click(QPoint const &pos)
609 if(m_editor->link_mode()) do_click_in_link_mode(pos);
612 void TM::TargetPanel::do_click_in_link_mode(QPoint const &pos)
614 Text::pointer w = select_word(pos);
617 WordLinker *wl = m_source_panel->linker();
618 wl->toggle(WordLink::Target, w);
619 m_source_panel->ensure_highlight();
620 setTextCursor(cursorForPosition(pos));
623 void TM::TargetPanel::do_focusin()
628 void TM::TargetPanel::do_focusout()
633 void TM::TargetPanel::do_key_press_in_link_mode(QKeyEvent *ev)
640 m_editor->set_link_mode(false);
646 TextPanel::keyPressEvent(ev);
651 // Editor ---------------------------------------------------------------------
653 TM::Editor::Editor(Settings *settings, Service *service, QWidget *parent)
655 , m_settings(settings)
660 , m_current_source_panel(nullptr)
662 TextArea *ta = text_area();
663 connect(ta, SIGNAL(focusInChild(TextPanel*,TextPanel*)), this, SLOT(onFocusInChild(TextPanel*,TextPanel*)));
667 * \brief エディタが保持しているリソースを適切に解放します。
669 void TM::Editor::clear()
671 if(m_segment_id < 0) return; // 文を保持していない場合、何もしない。
674 if(m_current_source_panel) do_panel_leave(m_current_source_panel);
675 m_current_source_panel = nullptr;
677 TextArea *ta = text_area();
680 set_link_mode(false);
681 parent_editor_widget()->set_link_mode_disabled(true);
686 void TM::Editor::set_segment(TextSegment::pointer segment)
689 m_segment_id = segment->segment_id();
690 TextArea *ta = text_area();
692 for(TextSentence::pointer p : *segment)
694 Text::pointer s = p->source_sentence();
695 Text::pointer t = p->target_sentence();
697 SourcePanel *sp = ta->append<SourcePanel>();
698 TargetPanel *tp = ta->append<TargetPanel>();
700 sp->set_target_panel(tp);
701 sp->set_editor(this);
702 tp->set_source_panel(sp);
703 tp->set_editor(this);
706 if(t) tp->set_sentence(t);
712 bool TM::Editor::edit_mode() const { return m_edit_mode; }
714 void TM::Editor::set_edit_mode(bool mode_) { m_edit_mode = mode_; }
716 bool TM::Editor::link_mode() const { return m_link_mode; }
718 void TM::Editor::set_link_mode(bool mode_)
720 if(mode_ == m_link_mode) return; // モードが変化していない場合、何もしない。
721 assert(!mode_ || can_link_mode()); // リンクモードに入るには、条件がある。
724 parent_editor_widget()->set_link_mode(mode_);
726 if(mode_) do_link_mode_enter(m_current_source_panel);
727 else if(m_current_source_panel) do_link_mode_leave(m_current_source_panel);
730 bool TM::Editor::can_link_mode() const
732 if(!m_current_source_panel) return false;
733 if(!edit_mode()) return false;
734 if(current_target_panel()->is_empty()) return false;
739 TM::EditorWidget* TM::Editor::parent_editor_widget()
741 EditorWidget* result = qobject_cast<EditorWidget*>(parentWidget());
746 TM::TargetPanel* TM::Editor::current_target_panel()
748 TargetPanel *result = nullptr;
749 if(m_current_source_panel) result = m_current_source_panel->target_panel();
753 TM::TargetPanel const* TM::Editor::current_target_panel() const
755 return const_cast<Editor*>(this)->current_target_panel();
758 void TM::Editor::onFocusInChild(TextPanel *new_, TextPanel *)
760 SourcePanel *old_panel = m_current_source_panel;
761 SourcePanel *new_panel = find_source_panel(new_);
764 if(old_panel && old_panel != new_panel) do_panel_leave(old_panel);
765 m_current_source_panel = new_panel;
766 if(old_panel != new_panel) do_panel_enter(new_panel);
769 TM::SourcePanel* TM::Editor::find_source_panel(TextPanel *panel)
771 SourcePanel *sp = qobject_cast<SourcePanel*>(panel);
773 TargetPanel *tp = qobject_cast<TargetPanel*>(panel);
774 if(tp) return tp->source_panel();
779 * \brief 文を表示するパネルにフォーカスが入るとき呼び出されます。
781 void TM::Editor::do_panel_enter(SourcePanel *panel)
783 TargetPanel *tp = panel->target_panel();
786 if(can_link_mode()) parent_editor_widget()->set_link_mode_disabled(false);
790 * \brief 文を表示するパネルからフォーカスが出るとき呼び出されます。
792 void TM::Editor::do_panel_leave(SourcePanel *panel)
794 TargetPanel *tp = panel->target_panel();
798 set_link_mode(false);
799 parent_editor_widget()->set_link_mode_disabled(true);
801 if(!tp->is_text_saved())
803 int tcode = parent_editor_widget()->target_language();
805 if(tp->is_text_dirty())
807 panel->linker()->clear();
808 QString string = tp->toPlainText();
809 Text::pointer sentences = m_service->divide_into_sentences(tcode, string);
810 if(sentences->size())
812 Text::pointer words = m_service->divide_into_words(tcode, sentences->begin());
813 tp->set_sentence(words); // ココで、text_dirtyがfalseになる。
817 parent_editor_widget()->save_sentence(m_segment_id, panel->index(),
818 tp->sentence(), panel->linker()->to_json_array());
819 tp->set_text_saved(true);
824 * \brief リンクモードに入るとき呼び出されます。
826 void TM::Editor::do_link_mode_enter(SourcePanel *panel)
829 TargetPanel *tp = panel->target_panel();
831 int tcode = parent_editor_widget()->target_language();
833 if(tp->is_text_dirty())
835 panel->linker()->clear();
836 QString string = tp->toPlainText();
837 Text::pointer sentences = m_service->divide_into_sentences(tcode, string);
838 if(sentences->size())
840 Text::pointer words = m_service->divide_into_words(tcode, sentences->begin());
841 tp->set_sentence(words); // ココで、text_dirtyがfalseになる。
845 panel->ensure_highlight();
849 * \brief リンクモードから出るとき呼び出されます。
851 void TM::Editor::do_link_mode_leave(SourcePanel *panel)
853 qDebug() << m_current_source_panel->linker()->to_json_array();
855 panel->commit_link();
856 //panel->clear_highlight();