diff --git a/CHANGES.md b/CHANGES.md index daf3b76..c410ebb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,31 @@ CHANGES +ver 2.1.1 + +This is a minor revision as most are under the hood changes + +- Change: hookup menu Undo/Redo items to stack, now items will display things like "Redo/Undo insert ..." + +- Change: remove unused actions from UI + +- Change: New qcommand `style_update` to update style changes, brings style changes into undo framework + +- Change: QUndoCommand text messages standardized + +- Change: Main editor and dialogs have new/condensed tooltips, docs updated. + +- Change: Toolbox Dock now uses tabs (holding scroll areas) rather than pages, less scrolling needed and more space for ui since page titles do not take up vertical space + +- Bug: word wrapped text did not return proper times for timestamps, added `split` method to elements + +- Bug: fixed RTF parser that did not respect spaces as delimiters vs text, now using new json format + +- Bug: fix #10 where editor falls back to default system font, should now fall back to first style font + +- Change: SRT export now word-wraps to 47 characters max automatically. + +- Change: gather UI updates into `update_gui` to update every cursor move + ver 2.1.0 This version focuses mainly on "things" to be inserted. diff --git a/docs/reference/commands.md b/docs/reference/commands.md index e150f1e..a706848 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -51,6 +51,12 @@ Attributes: - `space_placement`: value from Plover config - `add_space`: whether to add space upon merge, default is `True` +## style_update + +- `styles`: reference to style dict in editor +- `style_name`: name of style to be updated +- `new_style_dict`: dict containing new style attributes +- `old_style_dict`: dict with old style attributes ## set_par_style: diff --git a/docs/reference/elements.md b/docs/reference/elements.md index 3e553f9..b8e4d8c 100644 --- a/docs/reference/elements.md +++ b/docs/reference/elements.md @@ -28,6 +28,7 @@ It has the methods: - `__getitem__`: returns new instance after deepcopy - `__repr__`: representation as `dict` - `length`: returns length of string, here as placeholder in order to keep consistency with other subclassed elements, the functional length +- `split`: splits text string on whitespace (re from textwrapper), returns list of elements containing each text piece separately, but same otherwise as original - `from_dict`: can populate class using a dict - `to_display`: formatted string for display in GUI, should be a string for three lines, 1) icon letter, 2) element data, if any, 3) text - `to_json`: returns dict of attributes diff --git a/docs/reference/export.md b/docs/reference/export.md index 3db3143..659ccef 100644 --- a/docs/reference/export.md +++ b/docs/reference/export.md @@ -58,4 +58,28 @@ The Open Document Format (`ODF`) is an open source standard for documents based ### RTF/CRE -Plover2CAT offers exports to RTF/CRE, the commonly used data exchange format for transcripts. As a result, it is possible to import transcripts from Plover2CAT to commercial software such as CaseCatalyst. Plover2CAT exports a [subset](rtf_support.md) of the [RTF spec](https://web.archive.org/web/20201017075356/http://www.legalxml.org/workgroups/substantive/transcripts/cre-spec.htm). \ No newline at end of file +Plover2CAT offers exports to RTF/CRE, the commonly used data exchange format for transcripts. As a result, it is possible to import transcripts from Plover2CAT to commercial software such as CaseCatalyst. Plover2CAT exports a [subset](rtf_support.md) of the [RTF spec](https://web.archive.org/web/20201017075356/http://www.legalxml.org/workgroups/substantive/transcripts/cre-spec.htm). + +## Export from editor + +The editor offloads exporting to another thread through `documentWorker`. + +`documentWorker` takes: + +- `document`: copy of transcript in dict form (save transcript automatically to update) +- `path`: export file path +- `config`: transcript config +- `styles`: dict of styles for transcript +- `user_field_dict`: dict of fields +- `home_dir`: transcript dir path + +`documentWorker` has two signals: + +- `progress`: sent after generating a paragraph with paragraph number, updates progress bar in editor +- `finished`: sent after export file is created + +Each export format has its own method called from editor. + +### Wrappers and helpers + +Wrapping of paragraphs is done with `steno_wrap_*` after appropriate formatting with `format_*_text`, resulting in a `dict` containing `{line_num: line_data_dict}`. \ No newline at end of file diff --git a/docs/reference/main.md b/docs/reference/main.md index 5ba155d..5e7301a 100644 --- a/docs/reference/main.md +++ b/docs/reference/main.md @@ -9,11 +9,14 @@ The main class in Plover2CAT is PloverCATWindow that subclasses `QMainWindow` an - `recorder`: instance of `QAudioRecorder` - `config`: dict holding transcript configuration - `file_name`: path for transcript folder +- `backup_document`: dict of transcript data, updated on save - `styles`: dict holding styles - `txt_formats`: dict holding "full" font formatting info (after recursion) - `par_formats`: dict holding "full" paragraph formatting info (after recursion) - `user_field_dict`: dict, holds user defined fields - `auto_paragraph_affixes`: dict, holds affixes for styles +- `index_dialog`: index dialog editor +- `suggest_dialog`: suggestion dialog editor - `styles_path`: path referencing style file - `stroke_time`: text string timestamp of last stroke - `audio_file`: path referencing file being played/recorded @@ -22,6 +25,7 @@ The main class in Plover2CAT is PloverCATWindow that subclasses `QMainWindow` an - `last_raw_steno`: string, raw steno of last stroke - `last_string_sent`: string, text sent with last stroke - `last_backspaces_sent`: integer, number of backspaces sent with last stroke +- `track_lengths`: deque holding len of string_sent and backspaces_sent, used for tracking/comparing corrections if strokes need to be combined - `autosave_time`: `QTimer` object for activating autosave - `undo_stack`: holds `QUndoStack` - `cutcopy_storage`: `element_collection` holding steno to paste @@ -56,6 +60,7 @@ Methods that use manipulate the stroke data or use `QUndoCommands` are in *itali ### GUI - `set_shortcuts`: reads `shortcuts.json` and makes menu shortcuts as needed +- `edit_shortcuts`: opens shortcut dialog editor - `about`: displays version - `acknowledge`: displays acknowledgments - `open_help`: sends user to help docs @@ -77,17 +82,20 @@ Methods that use manipulate the stroke data or use `QUndoCommands` are in *itali - `navigate_to`: function accepts block number, moves and sets editor cursor to beginning of block - `update_gui`: collects other functions to be updated each time cursor changes - `update_navigation`: updates Navigation pane, displays list of heading paragraphs +- `update_index_menu`: generates sub-menu items for quick index entry insertion +- `set_autosave_time`: set autosave time interval ### Transcript management - `create_new`: creates new transcript project - *`open_file`*: opens existing transcript project - `save_file`: saves transcript project -- `save_transcript`: extracts transcript data from editor +- `save_transcript`: extracts transcript data from editor, only updates values if necessary, ie every par starting with first with `userState` == 1 - `dulwich_save`: commits transcript files to repo with commit message - *`load_transcript`*: loads transcript data into editor and `userData` in blocks - `revert_file`: reverts transcript back to selected commit from repo - *`save_as_file`*: saves transcript data and tape into new location +- `autosave`: saves present transcript to hidden file - `close_file`: closes transcript project and cleans up editor - `action_close`: quits editor window - `recentfile_open`: opens a recent file through `action` @@ -101,7 +109,7 @@ Methods that use manipulate the stroke data or use `QUndoCommands` are in *itali - `remove_dict`: file selection dialog to remove custom dict from `dict/` and plover dictionary stack - `set_dictionary_config`: takes list of dictionary paths, generate default dict if missing, backups present dictionary stack and loads transcript dictionaries - `restore_dictionary_from_backup`: restore plover dictionary stack from backup file - +- `transcript_suggest`: trigger suggestion dialog ### Config management - `load_config_file`: reads config file and sets editor UI variables @@ -116,12 +124,13 @@ Methods that use manipulate the stroke data or use `QUndoCommands` are in *itali - `gen_style_formats`: generates complete font and paragraph format dicts recursively for each style - `select_style_file`: load style fil from user file selction - `style_from_template`: reads ODF or RTF file, extracting only style information to write to new style file -- `display_block_data`: triggered manually after text changes or split/merge, updates style and block properties display, triggers autocomplete dropdown if toggled +- `display_block_data`: updates style and block properties display, triggers autocomplete dropdown if toggled - `display_block_steno`: takes strokes, update Reveal Steno dock with strokes, called from `display_block_data` - `refresh_steno_display`: updates Reveal Steno pane manually - *`update_paragraph_style`*: updates style of present paragraph block - `update_style_display`: updates UI elements to display present style - `style_edit`: changes properties of current style to user selections +- `check_undo_stack`: will trigger editor style refresh if undo/redo action is related to style changes - `new_style`: create a new style based on current style - *`refresh_editor_styles`*: complete refresh of all paragraph blocks based on present styles - *`to_next_styles`*: sets current block style based on `nextstylename` attribute of previous block if exists @@ -161,7 +170,10 @@ Methods that use manipulate the stroke data or use `QUndoCommands` are in *itali - *`edit_fields`*: calls `fieldDialogWindow` to create and edit user fields, and refreshes existing field elements in text - `add_begin_auto_affix`: checks and adds prefix set for `style`, copying `element` and returning `automatic_text` element - `add_end_auto_affix`: checks and adds suffix set for `style`, copying `element` and returning `automatic_text` element - +- `insert_index_entry`: create index element and insert into transcript +- `extract_indexes`: find all index entries in transcript +- `update_indices`: check and updates transcript index entries according to present indices from indices dialog +- `edit_indices`: opens indices editor dialog ### Search - `search`: wrapper function for three types of searches diff --git a/docs/reference/rtf_support.md b/docs/reference/rtf_support.md index 5c57079..5d4992c 100644 --- a/docs/reference/rtf_support.md +++ b/docs/reference/rtf_support.md @@ -55,7 +55,6 @@ Recognized RTF/CRE flags Timecode (\cxt) Steno (\cxs) Automatic Text (\cxa) - Delete space (\cxds) diff --git a/plover_cat/__version__.py b/plover_cat/__version__.py index 0873e16..3ead992 100644 --- a/plover_cat/__version__.py +++ b/plover_cat/__version__.py @@ -1,2 +1,2 @@ -__version__ = "2.1.0" +__version__ = "2.1.1" diff --git a/plover_cat/affix_dialog.ui b/plover_cat/affix_dialog.ui index 2ca93fe..41b1678 100644 --- a/plover_cat/affix_dialog.ui +++ b/plover_cat/affix_dialog.ui @@ -24,7 +24,11 @@ - + + + Style to set affixes for + + @@ -42,6 +46,9 @@ + + String to add at start of paragraph + false @@ -55,7 +62,11 @@ - + + + String to add to end of paragraph + + @@ -66,6 +77,9 @@ Qt::NoFocus + + Insert tab character into prefix/suffix string + Insert tab at cursor @@ -73,6 +87,9 @@ + + Save changes for selected style + Save diff --git a/plover_cat/affix_dialog_ui.py b/plover_cat/affix_dialog_ui.py deleted file mode 100644 index dd98533..0000000 --- a/plover_cat/affix_dialog_ui.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'plover_cat\affix_dialog.ui' -# -# Created by: PyQt5 UI code generator 5.15.9 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_affixDialog(object): - def setupUi(self, affixDialog): - affixDialog.setObjectName("affixDialog") - affixDialog.resize(387, 173) - self.verticalLayout = QtWidgets.QVBoxLayout(affixDialog) - self.verticalLayout.setObjectName("verticalLayout") - self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setObjectName("formLayout") - self.label = QtWidgets.QLabel(affixDialog) - self.label.setObjectName("label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) - self.styleName = QtWidgets.QComboBox(affixDialog) - self.styleName.setObjectName("styleName") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.styleName) - self.line = QtWidgets.QFrame(affixDialog) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.line) - self.label_3 = QtWidgets.QLabel(affixDialog) - self.label_3.setObjectName("label_3") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3) - self.prefixString = QtWidgets.QLineEdit(affixDialog) - self.prefixString.setClearButtonEnabled(False) - self.prefixString.setObjectName("prefixString") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.prefixString) - self.label_2 = QtWidgets.QLabel(affixDialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_2) - self.suffixString = QtWidgets.QLineEdit(affixDialog) - self.suffixString.setObjectName("suffixString") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.suffixString) - self.verticalLayout.addLayout(self.formLayout) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.insertTab = QtWidgets.QPushButton(affixDialog) - self.insertTab.setFocusPolicy(QtCore.Qt.NoFocus) - self.insertTab.setObjectName("insertTab") - self.horizontalLayout.addWidget(self.insertTab) - self.saveAffixes = QtWidgets.QPushButton(affixDialog) - self.saveAffixes.setObjectName("saveAffixes") - self.horizontalLayout.addWidget(self.saveAffixes) - self.verticalLayout.addLayout(self.horizontalLayout) - self.affixButtonBox = QtWidgets.QDialogButtonBox(affixDialog) - self.affixButtonBox.setOrientation(QtCore.Qt.Horizontal) - self.affixButtonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.affixButtonBox.setObjectName("affixButtonBox") - self.verticalLayout.addWidget(self.affixButtonBox) - - self.retranslateUi(affixDialog) - self.affixButtonBox.accepted.connect(affixDialog.accept) # type: ignore - self.affixButtonBox.rejected.connect(affixDialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(affixDialog) - - def retranslateUi(self, affixDialog): - _translate = QtCore.QCoreApplication.translate - affixDialog.setWindowTitle(_translate("affixDialog", "Dialog")) - self.label.setText(_translate("affixDialog", "Style:")) - self.label_3.setText(_translate("affixDialog", "Prefix string:")) - self.label_2.setText(_translate("affixDialog", "Suffix string:")) - self.insertTab.setText(_translate("affixDialog", "Insert tab at cursor")) - self.saveAffixes.setText(_translate("affixDialog", "Save")) diff --git a/plover_cat/documentWorker.py b/plover_cat/documentWorker.py index 0458c44..24588b9 100644 --- a/plover_cat/documentWorker.py +++ b/plover_cat/documentWorker.py @@ -8,7 +8,7 @@ from PyQt5.QtGui import QFontMetrics from time import sleep from plover import log -from plover_cat.helpers import save_json, ms_to_hours, return_commits, inch_to_spaces +from plover_cat.helpers import save_json, ms_to_hours, return_commits, inch_to_spaces, write_command from plover_cat.steno_objects import * from plover_cat.rtf_parsing import * from plover_cat.export_helpers import * @@ -485,8 +485,8 @@ def save_rtf(self): info_string.append(write_command("cxnoflines", value = page_vspan)) # cxlinex and cxtimex is hardcoded as it is also harcoded in odf # based on rtf spec, confusing whether left text margin, or left page margin - info_string.append(write_command("creatim", value = create_string)) - info_string.append(write_command("buptim", value = backup_string)) + info_string.append(write_command("creatim", value = create_string, group = True)) + info_string.append(write_command("buptim", value = backup_string, group = True)) info_string.append(write_command("cxlinex", value = int(in_to_twip(-0.15)))) info_string.append(write_command("cxtimex", value = int(in_to_twip(-1.5)))) info_string.append(write_command("cxnofstrokes", value = stroke_count)) @@ -510,27 +510,20 @@ def save_srt(self): line_num = 1 doc_lines = [] log.debug(f"Exporting in SRT to {self.path}") - for block_num, block_data in self.document.items(): - doc_lines += [block_num] - audiostarttime = block_data["audiostarttime"] - # webvtt uses periods for ms separator - audiostarttime = audiostarttime.replace(".", ",") - if "audioendtime" in block_data: - audioendtime = block_data["audioendtime"] - elif int(block_num) == (len(self.document) - 1): - log.debug(f"Block {block_num} does not have audioendtime. Last block in document. Setting 0 as timestamp.") - audioendtime = ms_to_hours(0) - else: - log.debug(f"Block {block_num} does not have audioendtime. Attempting to use starttime from next block.") - try: - audioendtime = self.document[str(int(block_num) + 1)]["audiostarttime"] - except (TypeError, KeyError) as err: - audioendtime = ms_to_hours(0) - audioendtime = audioendtime.replace(".", ",") - doc_lines += [audiostarttime + " --> " + audioendtime] - el_list = [ef.gen_element(element_dict = i, user_field_dict = self.user_field_dict) for i in block_data["strokes"]] - doc_lines += ["".join([el.to_text() for el in el_list])] - doc_lines += [""] + for block_num, block_data in self.document.items(): + if "audioendtime" not in block_data: + if str(int(block_num) + 1) in self.document and "audiostarttime" in self.document[str(int(block_num) + 1)]: + block_data["audioendtime"] = self.document[str(int(block_num) + 1)]["audiostarttime"] + else: + block_data["audioendtime"] = None + el_list = element_collection([ef.gen_element(element_dict = i, user_field_dict = self.user_field_dict) for i in block_data["strokes"]]) + par_dict = format_srt_text(el_list, line_num = line_num, audiostarttime = block_data["audiostarttime"], audioendtime = block_data["audioendtime"]) + line_num += len(par_dict) + for k, v in par_dict.items(): + doc_lines += [k] + doc_lines += [ms_to_hours(v["starttime"]).replace(".", ",") + " --> " + ms_to_hours(v["endtime"]).replace(".", ",")] + doc_lines += ["".join([el.to_text() for el in v["text"]])] + doc_lines += [""] self.progress.emit(int(block_num)) file_path = pathlib.Path(self.path) with open(file_path, "w", encoding="utf-8") as f: diff --git a/plover_cat/export_helpers.py b/plover_cat/export_helpers.py index 7b05d18..797de59 100644 --- a/plover_cat/export_helpers.py +++ b/plover_cat/export_helpers.py @@ -143,9 +143,26 @@ def format_text(block_data, style, max_char = 80, line_num = 0): par_text[k]["text"] = par_text[k]["text"] + "\n" * line_spaces return(par_text) +def format_srt_text(block_data, line_num = 0, audiostarttime = "", audioendtime = ""): + par_text = steno_wrap_srt(block_data, max_char = 47, starting_line_num = line_num) + # line_times = [] + # if audiostarttime and audioendtime: + # for line, data in par_txt.items(): + # line_times.append(data["starttime"]) + # line_times.append(data["endtime"]) + # if all([t >= audiostarttime and t <= audioendtime for t in line_times]): + # return(par_text) + # else: + # audiostarttime = hours_to_ms(audiostarttime) + # audioendtime = hours_to_ms(audioendtime) + # # not complete + return(par_text) + def steno_wrap_plain(text, block_data, max_char = 80, tab_space = 4, first_line_indent = "", - par_indent = "", timestamp = False, starting_line_num = 0): + par_indent = "", starting_line_num = 0): + """returns dict of dicts, {line_number: {line_text, line_timestamp}}""" # the -1 in max char is because the rounding is not perfect, might have some lines that just tip over + # uses text string instead of block_data because text has pre-expanded tabs wrapped = textwrap.wrap(text, width = max_char - 1, initial_indent= first_line_indent, subsequent_indent= par_indent, expand_tabs = False, tabsize = tab_space, replace_whitespace=False) begin_pos = 0 @@ -167,7 +184,8 @@ def steno_wrap_plain(text, block_data, max_char = 80, tab_space = 4, first_line_ return(par_dict) def steno_wrap_odf(block_data, max_char = 80, tab_space = 4, first_line_indent = "", - par_indent = "", timestamp = False, starting_line_num = 0): + par_indent = "", starting_line_num = 0): + """ returns dict of dicts {line_number: {line_text, line_timestamp}}""" # the -1 in max char is because the rounding is not perfect, might have some lines that just tip over wrapper = steno_wrapper(width = max_char - 1, initial_indent= first_line_indent, subsequent_indent= par_indent, expand_tabs = False, tabsize = tab_space, replace_whitespace=False) @@ -179,7 +197,23 @@ def steno_wrap_odf(block_data, max_char = 80, tab_space = 4, first_line_indent = par_dict[starting_line_num + ind + 1] = {"text": wrapped[ind], "time": line_time} return(par_dict) +def steno_wrap_srt(block_data, max_char = 47, tab_space = 0, first_line_indent = "", + par_indent = "", starting_line_num = 0): + # change from other wrapper here, tabs are expanded into 0 spaces + wrapper = steno_wrapper(width = max_char - 1, initial_indent= first_line_indent, + subsequent_indent= par_indent, expand_tabs = True, tabsize = tab_space, replace_whitespace=False) + wrapped = wrapper.wrap(text = block_data) + begin_pos = 0 + par_dict = {} + for ind, i in enumerate(wrapped): + ec = element_collection(i) + start_time = ec.audio_time() + end_time = ec.audio_time(reverse = True) + par_dict[starting_line_num + ind + 1] = {"text": wrapped[ind], "starttime": start_time, "endtime": end_time} + return(par_dict) + def load_odf_styles(path): + """extract styles from ODT file and convert supported to par + text style dicts""" log.debug(f"Loading ODF style file from {str(path)}") style_text = load(path) json_styles = {} @@ -209,6 +243,7 @@ def load_odf_styles(path): return(json_styles) def recursive_style_format(style_dict, style, prop = "paragraphproperties"): + """used to get full style par/text format dict if style inherits from another""" if "parentstylename" in style_dict[style]: parentstyle = recursive_style_format(style_dict, style_dict[style]["parentstylename"], prop = prop) if prop in style_dict[style]: @@ -221,6 +256,7 @@ def recursive_style_format(style_dict, style, prop = "paragraphproperties"): return({}) def parprop_to_blockformat(par_dict): + """take dict of paragraph attributes, returns QTextBlockFormat obj""" par_format = QTextBlockFormat() if "textalign" in par_dict: if par_dict["textalign"] == "justify": @@ -257,6 +293,7 @@ def parprop_to_blockformat(par_dict): return(par_format) def txtprop_to_textformat(txt_dict): + """takes dict of text attributes, returns QTextCharFormat obj""" txt_format = QTextCharFormat() if "fontfamily" in txt_dict: potential_font = QFont(txt_dict["fontfamily"]) diff --git a/plover_cat/field_dialog.ui b/plover_cat/field_dialog.ui index 5d7e832..4c7fdc6 100644 --- a/plover_cat/field_dialog.ui +++ b/plover_cat/field_dialog.ui @@ -27,7 +27,11 @@ - + + + Identifier for field, best be ASCII characters + + @@ -37,7 +41,11 @@ - + + + Text value for field, appears in transcript. If left blank, field name appears in text. + + @@ -53,6 +61,9 @@ Qt::DefaultContextMenu + + Double-click cells to edit field values. + QAbstractItemView::DoubleClicked @@ -80,6 +91,12 @@ + + false + + + Removes selected field from table + Remove field @@ -87,6 +104,9 @@ + + + Qt::Horizontal diff --git a/plover_cat/field_dialog_ui.py b/plover_cat/field_dialog_ui.py deleted file mode 100644 index d9cfeb6..0000000 --- a/plover_cat/field_dialog_ui.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'plover_cat\field_dialog.ui' -# -# Created by: PyQt5 UI code generator 5.15.9 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_fieldDialog(object): - def setupUi(self, fieldDialog): - fieldDialog.setObjectName("fieldDialog") - fieldDialog.setWindowModality(QtCore.Qt.WindowModal) - fieldDialog.resize(400, 300) - self.verticalLayout = QtWidgets.QVBoxLayout(fieldDialog) - self.verticalLayout.setObjectName("verticalLayout") - self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setObjectName("formLayout") - self.label = QtWidgets.QLabel(fieldDialog) - self.label.setObjectName("label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) - self.fieldName = QtWidgets.QLineEdit(fieldDialog) - self.fieldName.setObjectName("fieldName") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.fieldName) - self.label_2 = QtWidgets.QLabel(fieldDialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) - self.fieldValue = QtWidgets.QLineEdit(fieldDialog) - self.fieldValue.setObjectName("fieldValue") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.fieldValue) - self.addNewField = QtWidgets.QPushButton(fieldDialog) - self.addNewField.setObjectName("addNewField") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.addNewField) - self.verticalLayout.addLayout(self.formLayout) - self.userDictTable = QtWidgets.QTableWidget(fieldDialog) - self.userDictTable.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) - self.userDictTable.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked) - self.userDictTable.setTabKeyNavigation(False) - self.userDictTable.setProperty("showDropIndicator", False) - self.userDictTable.setDragDropOverwriteMode(False) - self.userDictTable.setAlternatingRowColors(True) - self.userDictTable.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.userDictTable.setObjectName("userDictTable") - self.userDictTable.setColumnCount(0) - self.userDictTable.setRowCount(0) - self.verticalLayout.addWidget(self.userDictTable) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.removeField = QtWidgets.QPushButton(fieldDialog) - self.removeField.setObjectName("removeField") - self.horizontalLayout.addWidget(self.removeField) - self.buttonBox = QtWidgets.QDialogButtonBox(fieldDialog) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout.addWidget(self.buttonBox) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(fieldDialog) - self.buttonBox.accepted.connect(fieldDialog.accept) # type: ignore - self.buttonBox.rejected.connect(fieldDialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(fieldDialog) - - def retranslateUi(self, fieldDialog): - _translate = QtCore.QCoreApplication.translate - fieldDialog.setWindowTitle(_translate("fieldDialog", "Field Editor")) - self.label.setText(_translate("fieldDialog", "Field name:")) - self.label_2.setText(_translate("fieldDialog", "Field value:")) - self.addNewField.setText(_translate("fieldDialog", "Add new field")) - self.userDictTable.setSortingEnabled(True) - self.removeField.setText(_translate("fieldDialog", "Remove field")) diff --git a/plover_cat/helpers.py b/plover_cat/helpers.py index c253d3f..0259a7e 100644 --- a/plover_cat/helpers.py +++ b/plover_cat/helpers.py @@ -6,7 +6,20 @@ from plover import log from dulwich.porcelain import open_repo_closing +def write_command(control, text = None, value = None, visible = True, group = False): + command = "\\" + control + if value is not None: + command = command + str(value) + if text: + command = command + " " + text + if not visible: + command = "\\*" + command + if group: + command = "{" + command + "}" + return(command) + def return_commits(repo, max_entries = 100): + """ Adapted from dulwich, returns commit info""" with open_repo_closing(repo) as r: walker = r.get_walker(max_entries = max_entries, paths=None, reverse=False) commit_strs = [] @@ -24,6 +37,13 @@ def ms_to_hours(millis): hours, minutes = divmod(minutes, 60) return ("%02d:%02d:%02d.%03d" % (hours, minutes, seconds, milliseconds)) +def hours_to_ms(hour_str): + """Converts formatted hour:min:sec.milli to milliseconds""" + hours, minutes, sec_ms = hour_str.split(":") + seconds, milliseconds = sec_md.split(".") + total_ms = milliseconds + seconds * 1000 + minutes * 60000 + hours * 3600000 + return(total_ms) + def in_to_pt(inch): inch = float(inch) return(inch * 72) @@ -90,6 +110,7 @@ def backup_dictionary_stack(dictionaries, path): pass def remove_empty_from_dict(d): + """removes dict key:value if value is None-like""" if type(d) is dict: return dict((k, remove_empty_from_dict(v)) for k, v in d.items() if v and remove_empty_from_dict(v)) elif type(d) is list: @@ -98,8 +119,9 @@ def remove_empty_from_dict(d): return d def hide_file(filename): + """helper for windows os to hide autosave file""" import ctypes FILE_ATTRIBUTE_HIDDEN = 0x02 ret = ctypes.windll.kernel32.SetFileAttributesW(filename, FILE_ATTRIBUTE_HIDDEN) - if not ret: # There was an error. + if not ret: raise ctypes.WinError() diff --git a/plover_cat/index_dialog.ui b/plover_cat/index_dialog.ui index 0d247ba..0636dde 100644 --- a/plover_cat/index_dialog.ui +++ b/plover_cat/index_dialog.ui @@ -28,10 +28,17 @@ - + + + Indices are numbered starting at 0 + + + + Add another index + Add New Index @@ -49,13 +56,20 @@ - + + + Prefix for index entry + + true + + Do not show entry description in transcript + Hide entry descriptions @@ -101,7 +115,11 @@ - + + + Text for the index entry + + @@ -124,6 +142,9 @@ false + + Save changes to present index and insert selected entry + Save && Insert @@ -131,6 +152,9 @@ + + Save changes to selected index + Save diff --git a/plover_cat/index_dialog_ui.py b/plover_cat/index_dialog_ui.py deleted file mode 100644 index ed68bda..0000000 --- a/plover_cat/index_dialog_ui.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'plover_cat\index_dialog.ui' -# -# Created by: PyQt5 UI code generator 5.15.9 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_indexDialog(object): - def setupUi(self, indexDialog): - indexDialog.setObjectName("indexDialog") - indexDialog.resize(448, 305) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(indexDialog) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.verticalLayout = QtWidgets.QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setObjectName("formLayout") - self.label = QtWidgets.QLabel(indexDialog) - self.label.setObjectName("label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.indexChoice = QtWidgets.QComboBox(indexDialog) - self.indexChoice.setObjectName("indexChoice") - self.horizontalLayout_3.addWidget(self.indexChoice) - self.addNewIndex = QtWidgets.QPushButton(indexDialog) - self.addNewIndex.setObjectName("addNewIndex") - self.horizontalLayout_3.addWidget(self.addNewIndex) - self.formLayout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_3) - self.label_2 = QtWidgets.QLabel(indexDialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.indexPrefix = QtWidgets.QLineEdit(indexDialog) - self.indexPrefix.setObjectName("indexPrefix") - self.horizontalLayout_4.addWidget(self.indexPrefix) - self.hideDescript = QtWidgets.QCheckBox(indexDialog) - self.hideDescript.setEnabled(True) - self.hideDescript.setChecked(True) - self.hideDescript.setTristate(False) - self.hideDescript.setObjectName("hideDescript") - self.horizontalLayout_4.addWidget(self.hideDescript) - self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_4) - self.verticalLayout.addLayout(self.formLayout) - self.label_3 = QtWidgets.QLabel(indexDialog) - self.label_3.setObjectName("label_3") - self.verticalLayout.addWidget(self.label_3) - self.displayEntries = QtWidgets.QTableWidget(indexDialog) - self.displayEntries.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked) - self.displayEntries.setObjectName("displayEntries") - self.displayEntries.setColumnCount(0) - self.displayEntries.setRowCount(0) - self.verticalLayout.addWidget(self.displayEntries) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.label_4 = QtWidgets.QLabel(indexDialog) - self.label_4.setObjectName("label_4") - self.horizontalLayout.addWidget(self.label_4) - self.entryText = QtWidgets.QLineEdit(indexDialog) - self.entryText.setObjectName("entryText") - self.horizontalLayout.addWidget(self.entryText) - self.entryAdd = QtWidgets.QPushButton(indexDialog) - self.entryAdd.setEnabled(False) - self.entryAdd.setObjectName("entryAdd") - self.horizontalLayout.addWidget(self.entryAdd) - self.verticalLayout.addLayout(self.horizontalLayout) - self.horizontalLayout_2.addLayout(self.verticalLayout) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.saveAndInsert = QtWidgets.QPushButton(indexDialog) - self.saveAndInsert.setEnabled(False) - self.saveAndInsert.setObjectName("saveAndInsert") - self.verticalLayout_2.addWidget(self.saveAndInsert) - self.saveIndex = QtWidgets.QPushButton(indexDialog) - self.saveIndex.setObjectName("saveIndex") - self.verticalLayout_2.addWidget(self.saveIndex) - self.buttonBox = QtWidgets.QDialogButtonBox(indexDialog) - self.buttonBox.setOrientation(QtCore.Qt.Vertical) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Close) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout_2.addWidget(self.buttonBox) - self.horizontalLayout_2.addLayout(self.verticalLayout_2) - - self.retranslateUi(indexDialog) - self.buttonBox.rejected.connect(indexDialog.hide) # type: ignore - QtCore.QMetaObject.connectSlotsByName(indexDialog) - - def retranslateUi(self, indexDialog): - _translate = QtCore.QCoreApplication.translate - indexDialog.setWindowTitle(_translate("indexDialog", "Dialog")) - self.label.setText(_translate("indexDialog", "Index:")) - self.addNewIndex.setText(_translate("indexDialog", "Add New Index")) - self.label_2.setText(_translate("indexDialog", "Prefix:")) - self.hideDescript.setText(_translate("indexDialog", "Hide entry descriptions")) - self.label_3.setText(_translate("indexDialog", "Entries for index:")) - self.displayEntries.setToolTip(_translate("indexDialog", "Double-click to edit index entry descriptions.")) - self.displayEntries.setSortingEnabled(True) - self.label_4.setText(_translate("indexDialog", "Index entry text:")) - self.entryAdd.setText(_translate("indexDialog", "Add new entry")) - self.saveAndInsert.setText(_translate("indexDialog", "Save && Insert")) - self.saveIndex.setText(_translate("indexDialog", "Save")) diff --git a/plover_cat/main_window.py b/plover_cat/main_window.py index 65ace6a..7765185 100644 --- a/plover_cat/main_window.py +++ b/plover_cat/main_window.py @@ -134,6 +134,25 @@ def __init__(self, engine): self.track_lengths = deque(maxlen = 10) self.autosave_time = QTimer() self.undo_stack = QUndoStack(self) + self.actionUndo = self.undo_stack.createUndoAction(self) + undo_icon = QtGui.QIcon() + undo_icon.addFile(":/arrow-curve-180.png", QtCore.QSize(), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.actionUndo.setIcon(undo_icon) + self.actionUndo.setShortcutContext(QtCore.Qt.WindowShortcut) + self.actionUndo.setToolTip("Undo writing or other action") + self.actionUndo.setShortcut("Ctrl+Z") + self.actionUndo.setObjectName("actionUndo") + self.actionRedo = self.undo_stack.createRedoAction(self) + redo_icon = QtGui.QIcon() + redo_icon.addFile(":/arrow-curve.png", QtCore.QSize(), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.actionRedo.setIcon(redo_icon) + self.actionRedo.setShortcutContext(QtCore.Qt.WindowShortcut) + self.actionRedo.setToolTip("Redo writing or other action") + self.actionRedo.setShortcut("Ctrl+Y") + self.actionRedo.setObjectName("actionRedo") + self.menuEdit.addSeparator() + self.menuEdit.addAction(self.actionUndo) + self.menuEdit.addAction(self.actionRedo) self.undoView.setStack(self.undo_stack) self.cutcopy_storage = {} self.repo = None @@ -188,8 +207,9 @@ def __init__(self, engine): self.actionCopy.triggered.connect(lambda: self.copy_steno()) self.actionCut.triggered.connect(lambda: self.cut_steno()) self.actionPaste.triggered.connect(lambda: self.paste_steno()) - self.actionRedo.triggered.connect(self.undo_stack.redo) - self.actionUndo.triggered.connect(self.undo_stack.undo) + self.undo_stack.indexChanged.connect(self.check_undo_stack) + # self.actionRedo.triggered.connect(self.undo_stack.redo) + # self.actionUndo.triggered.connect(self.undo_stack.undo) self.actionFindReplacePane.triggered.connect(lambda: self.show_find_replace()) self.actionJumpToParagraph.triggered.connect(self.jump_par) self.navigationList.itemDoubleClicked.connect(self.heading_navigation) @@ -510,7 +530,9 @@ def jump_par(self): def show_find_replace(self): if self.textEdit.textCursor().hasSelection() and self.search_text.isChecked(): self.search_term.setText(self.textEdit.textCursor().selectedText()) - self.toolBox.setCurrentWidget(self.find_replace_pane) + if not self.dockProp.isVisible(): + self.dockProp.setVisible(True) + self.tabWidget.setCurrentWidget(self.find_replace_pane) log.debug("User set find pane visible.") def heading_navigation(self, item): @@ -532,6 +554,8 @@ def update_gui(self): current_cursor = self.textEdit.textCursor() if current_cursor.block().userData(): self.display_block_steno(current_cursor.block().userData()["strokes"]) + self.update_style_display(current_cursor.block().userData()["style"]) + self.display_block_data() # skip if still on same block if self.cursor_block == self.textEdit.textCursor().blockNumber(): log.debug("Cursor in same paragraph as previous.") @@ -655,8 +679,11 @@ def create_new(self): self.update_paragraph_style() document_cursor = self.textEdit.textCursor() document_cursor.setBlockFormat(self.par_formats[self.style_selector.currentText()]) - document_cursor.setCharFormat(self.txt_formats[self.style_selector.currentText()]) - self.textEdit.setTextCursor(document_cursor) + document_cursor.setCharFormat(self.txt_formats[self.style_selector.currentText()]) + self.textEdit.setCurrentCharFormat(self.txt_formats[self.style_selector.currentText()]) + self.textEdit.setTextCursor(document_cursor) + self.textEdit.document().setDefaultFont(self.txt_formats[self.style_selector.currentText()].font()) + self.undo_stack.clear() def open_file(self, file_path = ""): self.mainTabs.setCurrentIndex(1) @@ -743,7 +770,8 @@ def open_file(self, file_path = ""): document_cursor = self.textEdit.textCursor() document_cursor.setBlockFormat(self.par_formats[self.style_selector.currentText()]) document_cursor.setCharFormat(self.txt_formats[self.style_selector.currentText()]) - self.textEdit.setTextCursor(document_cursor) + self.textEdit.document().setDefaultFont(self.txt_formats[self.style_selector.currentText()].font()) + self.undo_stack.clear() def save_file(self): if not self.file_name: @@ -773,7 +801,7 @@ def save_transcript(self, path): if status == 1: block_dict = deepcopy(block.userData().return_all()) block_num = block.blockNumber() - print(f"Paragraph {block_num} changed.") + # print(f"Paragraph {block_num} changed.") block_dict["strokes"] = block_dict["strokes"].to_json() json_document[str(block_num)] = block_dict block.setUserState(-1) @@ -1033,7 +1061,7 @@ def recentfile_store(self, path): except ValueError: pass recent_file_paths.insert(0, path) - print(recent_file_paths) + # print(recent_file_paths) # only remember last ten del recent_file_paths[10:] settings.setValue("recentfiles", recent_file_paths) @@ -1370,7 +1398,7 @@ def display_block_data(self): block_data = current_cursor.block().userData() log.debug(f"Update GUI to display block {block_number} data") if not block_data: - block_data = BlockUserData() + return self.editorParagraphLabel.setText(str(block_number)) if block_data["creationtime"]: self.editorCreationTime.setDateTime(QDateTime.fromString(block_data["creationtime"], "yyyy-MM-ddTHH:mm:ss.zzz")) @@ -1492,6 +1520,9 @@ def style_edit(self): # this is important so a style is not based on itself if self.blockParentStyle.currentText() != style_name: new_style_dict["parentstylename"] = self.blockParentStyle.currentText() + else: + QMessageBox.warning(self, "Edit style", "Style cannot be parent of itself.") + return if self.blockNextStyle.currentIndex() != -1: new_style_dict["nextstylename"] = self.blockNextStyle.currentText() if self.blockHeadingLevel.currentText() != "": @@ -1524,18 +1555,30 @@ def style_edit(self): # do not set if tabstop = 0, weird things might happen if tab_pos: new_par_dict["tabstop"] = "%.2fin" % tab_pos - original_style_txt.update(new_txt_dict) - original_style_txt = remove_empty_from_dict(original_style_txt) - original_style_par.update(new_par_dict) - log.debug("New paragraph properties: %s" % original_style_par) - log.debug("New text properties: %s" % original_style_txt) - new_style_dict["paragraphproperties"] = original_style_par - new_style_dict["textproperties"] = original_style_txt - log_dict = {"action": "edit_style", "style_dict": new_style_dict} - log.info(f"Style: {log_dict}") - self.styles[style_name] = new_style_dict - self.gen_style_formats() - self.refresh_editor_styles() + min_txt_style = {} + for k, v in new_txt_dict.items(): + if k in original_style_txt and v == original_style_txt[k]: + continue + else: + min_txt_style[k] = v + min_par_style = {} + for k, v in new_par_dict.items(): + if k in original_style_par and v == original_style_par[k]: + continue + else: + min_par_style[k] = v + # original_style_txt.update(new_txt_dict) + # original_style_txt = remove_empty_from_dict(original_style_txt) + # original_style_par.update(new_par_dict) + new_style_dict["paragraphproperties"] = min_par_style + new_style_dict["textproperties"] = min_txt_style + style_cmd = style_update(self.styles, style_name, new_style_dict) + self.undo_stack.push(style_cmd) + # self.refresh_editor_styles() + + def check_undo_stack(self, index): + if self.undo_stack.undoText().startswith("Style:") or self.undo_stack.redoText().startswith("Style:"): + self.refresh_editor_styles() def new_style(self): log.debug("User create new style") @@ -1558,25 +1601,37 @@ def refresh_editor_styles(self): user_choice = QMessageBox.question(self, "Refresh styles", f"There are {self.textEdit.document().blockCount()} paragraphs. Style refreshing may take some time. Continue?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if user_choice == QMessageBox.No: return + self.gen_style_formats() block = self.textEdit.document().begin() + current_cursor = self.textEdit.textCursor() self.progressBar = QProgressBar(self) self.progressBar.setMaximum(self.textEdit.document().blockCount()) self.progressBar.setFormat("Re-style paragraph %v") self.statusBar.addWidget(self.progressBar) - self.undo_stack.beginMacro("Refresh editor.") + self.progressBar.show() while True: try: block_style = block.userData()["style"] except TypeError: - block_style = "" - style_cmd = set_par_style(block.blockNumber(), block_style, self.textEdit, self.par_formats, self.txt_formats) - self.undo_stack.push(style_cmd) + # block_style = "" + continue + current_cursor.setPosition(block.position()) + current_cursor.movePosition(QTextCursor.StartOfBlock) + current_cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) + current_cursor.setBlockFormat(self.par_formats[block_style]) + it = block.begin() + while not it.atEnd(): + frag = it.fragment() + if frag.isValid() and not frag.charFormat().isImageFormat(): + current_cursor.setPosition(frag.position()) + current_cursor.setPosition(frag.position() + frag.length(), QTextCursor.KeepAnchor) + current_cursor.setCharFormat(self.txt_formats[block_style]) + it += 1 self.progressBar.setValue(block.blockNumber()) QApplication.processEvents() if block == self.textEdit.document().lastBlock(): break block = block.next() - self.undo_stack.endMacro() self.statusBar.removeWidget(self.progressBar) def to_next_style(self): @@ -1922,7 +1977,7 @@ def on_stroke(self, stroke_pressed): current_strokes = current_cursor.block().userData()["strokes"] start_pos = backtrack_coord(current_cursor.positionInBlock(), backspaces_sent, current_strokes.lens(), current_strokes.lengths()) - self.undo_stack.beginMacro(f"Remove {backspaces_sent} backspaces(s).") + self.undo_stack.beginMacro(f"Remove: {backspaces_sent} backspaces(s).") if start_pos < 0: log.debug(f"{start_pos} backspaces than exists on current paragraph.") while start_pos < 0: @@ -1955,7 +2010,7 @@ def on_stroke(self, stroke_pressed): if "\n" in string_sent and self.last_string_sent != "\n": list_segments = string_sent.splitlines(True) self.track_lengths.append(len(self.last_string_sent)) - self.undo_stack.beginMacro("Insert Group") + self.undo_stack.beginMacro(f"Insert: {string_sent}") for i, segment in enumerate(list_segments): stroke = stroke_text(time = self.stroke_time, stroke = self.last_raw_steno, text = segment.rstrip("\n")) # because this is all occurring in one stroke, only first segment gets the stroke @@ -1996,7 +2051,6 @@ def on_stroke(self, stroke_pressed): stroke) self.undo_stack.push(insert_cmd) self.last_string_sent = "" - self.display_block_data() self.textEdit.document().setModified(True) self.statusBar.clearMessage() @@ -2068,7 +2122,7 @@ def cut_steno(self, store = True): self.cutcopy_storage = block_data["strokes"].extract_steno(start_pos, stop_pos) log.debug("Data stored for pasting") self.statusBar.showMessage("Cut from paragraph {par_num}, from {start} to {end}".format(par_num = current_block_num, start = start_pos, end = stop_pos)) - self.undo_stack.beginMacro("Cut") + self.undo_stack.beginMacro(f"Cut: {selected_text}") remove_cmd = steno_remove(self.textEdit, current_block_num, start_pos, len(selected_text)) self.undo_stack.push(remove_cmd) @@ -2087,7 +2141,7 @@ def paste_steno(self): current_block_num = current_cursor.blockNumber() current_block = self.textEdit.document().findBlockByNumber(current_block_num) start_pos = min(current_cursor.position(), current_cursor.anchor()) - current_block.position() - self.undo_stack.beginMacro("Paste") + self.undo_stack.beginMacro(f"Paste: {store_data.to_text()}") self.textEdit.blockSignals(True) for el in store_data.data: current_block = self.textEdit.textCursor().blockNumber() @@ -2280,7 +2334,7 @@ def insert_text(self): current_block_num = current_cursor.blockNumber() current_block = self.textEdit.document().findBlockByNumber(current_block_num) start_pos = min(current_cursor.position(), current_cursor.anchor()) - current_block.position() - self.undo_stack.beginMacro("Insert normal text") + self.undo_stack.beginMacro(f"Insert: {text}") fake_steno = text_element(text = text) insert_cmd = steno_insert(self.textEdit, current_block_num, start_pos, fake_steno) self.undo_stack.push(insert_cmd) @@ -2313,10 +2367,8 @@ def edit_fields(self): new_field_dict = self.field_dialog.user_field_dict # after user_field_dict is set, then refresh current_cursor = self.textEdit.textCursor() - self.undo_stack.beginMacro("Refresh fields.") update_cmd = update_field(self.textEdit, current_cursor.blockNumber(), current_cursor.positionInBlock(), self.user_field_dict, new_field_dict) self.undo_stack.push(update_cmd) - self.undo_stack.endMacro() def add_begin_auto_affix(self, element, style): if style not in self.auto_paragraph_affixes: @@ -2352,7 +2404,7 @@ def insert_index_entry(self, el = None, action = None): el = index_text(prefix = index_prefix, indexname = index_name, hidden = index_hidden, text = txt) start_pos = current_cursor.positionInBlock() current_block = current_cursor.blockNumber() - self.undo_stack.beginMacro("Insert index") + self.undo_stack.beginMacro("Insert: index entry") if current_cursor.hasSelection() and el == None: self.cut_steno(store = False) self.textEdit.setTextCursor(current_cursor) @@ -2616,7 +2668,7 @@ def replace(self, to_next = True, steno = "", replace_term = None): replace_term = self.replace_term.text() if self.textEdit.textCursor().hasSelection(): log.debug("Replace %s with %s", self.textEdit.textCursor().selectedText(), replace_term) - self.undo_stack.beginMacro("Replace") + self.undo_stack.beginMacro(f"Replace: {self.textEdit.textCursor().selectedText()} with {replace_term}") current_cursor = self.textEdit.textCursor() current_block = current_cursor.block() start_pos = min(current_cursor.position(), current_cursor.anchor()) - current_block.position() @@ -3083,6 +3135,7 @@ def import_rtf(self): self.statusBar.showMessage("Parsing RTF.") self.progressBar = QProgressBar(self) self.statusBar.addWidget(self.progressBar) + # self.progressBar.show() parse_results = rtf_steno(selected_file[0], self.progressBar) QApplication.setOverrideCursor(Qt.WaitCursor) parse_results.parse_document() @@ -3090,7 +3143,7 @@ def import_rtf(self): style_dict, renamed_indiv_style = load_rtf_styles(parse_results) rtf_paragraphs = parse_results.paragraphs for ind, par in rtf_paragraphs.items(): - par["data"]["style"] = renamed_indiv_style[int(ind)] + par["style"] = renamed_indiv_style[int(ind)] file_path = pathlib.Path(pathlib.Path(selected_file[0]).name).with_suffix(".transcript") file_path = self.file_name / file_path save_json(rtf_paragraphs, file_path) diff --git a/plover_cat/plover_cat.ui b/plover_cat/plover_cat.ui index 1be4701..e0b1809 100644 --- a/plover_cat/plover_cat.ui +++ b/plover_cat/plover_cat.ui @@ -6,7 +6,7 @@ 0 0 - 956 + 955 743 @@ -71,7 +71,7 @@ true - + @@ -147,7 +147,8 @@ - 8 + Courier New + 12 @@ -187,7 +188,7 @@ 0 0 - 956 + 955 19 @@ -244,9 +245,6 @@ - - - @@ -331,6 +329,9 @@ Steno Actions + + true + @@ -348,6 +349,9 @@ Styling + + true + @@ -360,6 +364,9 @@ Insert + + true + Field... @@ -412,7 +419,7 @@ - 95 + 100 142 @@ -464,15 +471,15 @@ position. QPlainTextEdit::NoWrap - - 50.000000000000000 - true + + 50.000000000000000 + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -505,7 +512,7 @@ position. 185 - 195 + 139 @@ -608,8 +615,8 @@ position. - 264 - 89 + 256 + 100 @@ -748,8 +755,8 @@ position. - 87 - 289 + 137 + 165 @@ -773,1587 +780,1375 @@ position. - - - Qt::NoContextMenu - - - + + + + 0 + 0 + - - QFrame::NoFrame + + Qt::NoFocus - - QFrame::Plain + + QTabWidget::East - 0 + 1 + + + Qt::ElideRight + + + true + + + false + + + false + + + false - - - 0 - 0 - 363 - 472 - - - - Set style for current paragraph or load styles for export - :/edit-style.png:/edit-style.png - - Styling + + Style - - - - - Name of current style file - - - Style file location - - - - - - - Current Paragraph Style: - - - + + Set style for current paragraph or load styles for export + + - - - Qt::NoFocus + + + true + + + + 0 + 0 + 388 + 466 + + + + + + + Name of current style file + + + Style file location + + + + + + + Current Paragraph Style: + + + + + + + Qt::NoFocus + + + + + + + Style Settings + + + + + + + + Qt::ClickFocus + + + + Courier New + 12 + + + + + + + + Qt::ClickFocus + + + pt + + + 12 + + + + + + + + + + + Qt::NoFocus + + + Set text italic for style + + + + + + + :/edit-bold.png:/edit-bold.png + + + true + + + + + + + Qt::NoFocus + + + Set text italicized for style + + + + + + + :/edit-italic.png:/edit-italic.png + + + true + + + + + + + Qt::NoFocus + + + Set text underlined for style + + + + + + + :/edit-underline.png:/edit-underline.png + + + true + + + + + + + Qt::NoFocus + + + Set alignment left for style + + + + + + + :/edit-alignment.png:/edit-alignment.png + + + true + + + true + + + blockAlignment + + + + + + + Qt::NoFocus + + + Set alignment center for style + + + + + + + :/edit-alignment-center.png:/edit-alignment-center.png + + + true + + + blockAlignment + + + + + + + Qt::NoFocus + + + Set alignment right for style + + + + + + + :/edit-alignment-right.png:/edit-alignment-right.png + + + true + + + blockAlignment + + + + + + + Qt::NoFocus + + + Set alignment justified for style + + + + + + + :/edit-alignment-justify-distribute.png:/edit-alignment-justify-distribute.png + + + true + + + blockAlignment + + + + + + + Qt::NoFocus + + + Set line spacing as proportion of font height (ie 200% for double space) + + + % + + + 1000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Average Char Width + + + + + + + Qt::ClickFocus + + + Average character width of font, use this to estimate indent and margin positions. Less accurate with proportional fonts. + + + true + + + QAbstractSpinBox::NoButtons + + + in + + + + + + + First Line Indent + + + + + + + Qt::ClickFocus + + + Specify text indent in inches relative to page margin + + + in + + + 0.000000000000000 + + + 0.050000000000000 + + + + + + + Tab Stop Distance + + + + + + + Qt::ClickFocus + + + Specify position of tab stop + + + in + + + 0.050000000000000 + + + + + + + Left Margin (Indent) + + + + + + + Qt::ClickFocus + + + Specify paragraph margin relative to left page margin + + + in + + + 0.000000000000000 + + + 0.050000000000000 + + + + + + + Right Margin (Indent) + + + + + + + Qt::ClickFocus + + + Specify right margin relative to right page margin + + + in + + + 0.000000000000000 + + + 0.050000000000000 + + + + + + + Top Margin (Padding) + + + + + + + Qt::ClickFocus + + + Specify padding for top of paragraph + + + in + + + 0.000000000000000 + + + 0.050000000000000 + + + + + + + Bottom Margin (Padding) + + + + + + + Qt::ClickFocus + + + Specify padding for bottom of paragraph + + + false + + + in + + + 0.050000000000000 + + + + + + + Parent Style + + + + + + + Qt::NoFocus + + + Select parent style for style + + + + + + + Next Style + + + + + + + Qt::NoFocus + + + Select style of next paragraph + + + + + + + Heading Level + + + + + + + true + + + Qt::NoFocus + + + Specify heading level of style + + + + + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + + + + Qt::NoFocus + + + Modify Current Style + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + :/document-resize-actual.png:/document-resize-actual.png + + + Page Format + + + Set page format + + - - - Style Settings + + + true - - - - - - - Qt::ClickFocus - - - - Courier New - 12 - - - - - - - - Qt::ClickFocus - - - pt - - - 12 - - - - - - - - - - - Qt::NoFocus - - - Set text italic for style - - - - - - - :/edit-bold.png:/edit-bold.png - - - true - - - - - - - Qt::NoFocus - - - Set text italicized for style - - - - - - - :/edit-italic.png:/edit-italic.png - - - true - - - - - - - Qt::NoFocus - - - Set text underlined for style - - - - - - - :/edit-underline.png:/edit-underline.png - - - true - - - - - - - Qt::NoFocus - - - Set alignment left for style - - - - - - - :/edit-alignment.png:/edit-alignment.png - - - true - - - true - - - blockAlignment - - - - - - - Qt::NoFocus - - - Set alignment center for style - - - - - - - :/edit-alignment-center.png:/edit-alignment-center.png - - - true - - - blockAlignment - - - - - - - Qt::NoFocus - - - Set alignment right for style - - - - - - - :/edit-alignment-right.png:/edit-alignment-right.png - - - true - - - blockAlignment - - - - - - - Qt::NoFocus - - - Set alignment justified for style - - - - - - - :/edit-alignment-justify-distribute.png:/edit-alignment-justify-distribute.png - - - true - - - blockAlignment - - - - - - - Qt::NoFocus - - - Set line spacing as proportion of font height -(ie 200% for double space) - - - % - - - 1000 - - - 100 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Average Char Width - - - - - - - Qt::ClickFocus - - - Average character width of font, use this to estimate indent -and margin positions. Less accurate with proportional fonts. - - - true - - - QAbstractSpinBox::NoButtons - - - in - - - - - - - First Line Indent - - - - - - - Qt::ClickFocus - - - Specify text indent in inches relative to page margin - - - in - - - 0.000000000000000 - - - 0.050000000000000 - - - - - - - Tab Stop Distance - - - - - - - Qt::ClickFocus - - - Specify position of tab stop - - - in - - - 0.050000000000000 - - - - - - - Left Margin (Indent) - - - - - - - Qt::ClickFocus - - - Specify paragraph margin relative to left page margin - - - in - - - 0.000000000000000 - - - 0.050000000000000 - - - - - - - Right Margin (Indent) - - - - - - - Qt::ClickFocus - - - Specify right margin relative to right page margin - - - in - - - 0.000000000000000 - - - 0.050000000000000 - - - - - - - Top Margin (Padding) - - - - - - - Qt::ClickFocus - - - Specify padding for top of paragraph - - - in - - - 0.000000000000000 - - - 0.050000000000000 - - - - - - - Bottom Margin (Padding) - - - - - - - Qt::ClickFocus - - - Specify padding for bottom of paragraph - - - false - - - in - - - 0.050000000000000 - - - - - - - Parent Style - - - - - - - Qt::NoFocus - - - Select parent style for style - - - - - - - Next Style - - - - - - - Qt::NoFocus - - - Select style of next paragraph - - - - - - - Heading Level - - - - - - - true - - - Qt::NoFocus - - - Specify heading level of style - - + + + + 0 + -169 + 388 + 409 + + + + + + + - + Page Width - - + + + + + + Paper width + + + in. + + + 8.500000000000000 + + + + + - 1 + Page Height - - + + + + + + Page height + + + in. + + + 11.000000000000000 + + + + + - 2 + Left Margin - - + + + + + + Page left margin + + + in. + + + 4 + + + 1.750000000000000 + + + + + - 3 + Top Margin - - + + + + + + Page top margin + + + in. + + + 4 + + + 0.787400000000000 + + + + + - 4 + Right Margin - - + + + + + + Page right margin + + + in. + + + 4 + + + 0.379900000000000 + + + + + - 5 + Bottom Margin - - + + + + + + Page bottom margin + + + in. + + + 4 + + + 0.787400000000000 + + + + + - 6 + Max Char per Line - - + + + + + + Each line can contain at most n char, excluding line number and timestamps + + + Automatic + + + + + - 7 + Max Lines per Page - - + + + + + + Automatic + + + + + - 8 + Line Numbering + + + + + + + Enable line number in applicable formats - - - 9 + - - + + + + - 10 + Frequency - - - - - - - Qt::NoFocus - - - Modify Current Style - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - 0 - 0 - 410 - 415 - - - - Set page format for ODF export - - - - :/document-resize-actual.png:/document-resize-actual.png - - - Page Format - - - - - - - - Page Width - - - - - - - Paper width - - - in. - - - 8.500000000000000 - - - - - - - Page Height - - - - - - - Page height - - - in. - - - 11.000000000000000 - - - - - - - Left Margin - - - - - - - Page left margin - - - in. - - - 4 - - - 1.750000000000000 - - - - - - - Top Margin - - - - - - - Page top margin - - - in. - - - 4 - - - 0.787400000000000 - - - - - - - Right Margin - - - - - - - Page right margin - - - in. - - - 4 - - - 0.379900000000000 - - - - - - - Bottom Margin - - - - - - - Page bottom margin - - - in. - - - 4 - - - 0.787400000000000 - - - - - - - Max Char per Line - - - - - - - Each line can contain at most n char, -excluding line number and timestamps - - - Automatic - - - - - - - Max Lines per Page - - - - - - - Automatic - - - - - - - Line Numbering - - - - - - - Enable line number in applicable formats - - - - - - - - - - Frequency - - - - - - - Show line number every nth line for ODF - - - 1 - - - - - - - Line Timestamp - - - - - - - - - - - - - - - - Header: - - - - - - - - - Qt::ClickFocus - - - Header text to be aligned left. -Use %p for page number. - - - Left - - - - - - - Qt::ClickFocus - - - Header text to be centered. -Use %p for page number. - - - Center - - - - - - - Qt::ClickFocus - - - Header text to be aligned right. -Use %p for page number. - - - Right - - - - - - - - - Footer: - - - - - - - - - Qt::ClickFocus - - - Footer text to be aligned left. -Use %p for page number. - - - Left - - - - - - - Qt::ClickFocus - - - Footer text to be aligned center. -Use %p for page number. - - - Center - - - - - - - Qt::ClickFocus - - - Footer text to be aligned right. -Use %p for page number. - - - Right - - - - - - - - - Qt::NoFocus - - - Confirm and save changes to page and margin parameters - - - Change Page Layout - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - 0 - 0 - 300 - 217 - - - - Find and replace - - - - :/magnifier.png:/magnifier.png - - - Find and Replace - - - - - - - - Search Types - - - - - - Search visible text for match - - - Text - - - true - - - - - - - Search strokes, find text has to match stroke completely - - - Underlying Steno - - - - - - - Search what appears to be untranslated chords in visible text - - - Untrans - - - - - - - - - - - - Text to search for - - - Find - - - - - - - Perform selected search forwards - - - Next - - + + + + + + Show line number every nth line for ODF + + + 1 + + + + + + + Line Timestamp + + + + + + + + + + + - - - Perform selected search backwards - + - Previous + Header: - - - - - - - The text to replace the match found in search - - - Replace - - + + + + + Qt::ClickFocus + + + Header text to be aligned left. Use %p for page number. + + + Left + + + + + + + Qt::ClickFocus + + + Header text to be centered. Use %p for page number. + + + Center + + + + + + + Qt::ClickFocus + + + Header text to be aligned right. Use %p for page number. + + + Right + + + + - - - Replace the found match with the text in the replace textbox - + - Once + Footer: - - - Replace all matches with text in replace textbox - - - All - - - - - - - - - - - Case sensitive search - - - Match Case - - + + + + + Qt::ClickFocus + + + Footer text to be aligned left. Use %p for page number. + + + Left + + + + + + + Qt::ClickFocus + + + Footer text to be aligned center. Use %p for page number. + + + Center + + + + + + + Qt::ClickFocus + + + Footer text to be aligned right. Use %p for page number. + + + Right + + + + - - - Text in find has to match text as a whole word/stroke - - - Whole Word/Stroke + + + Qt::NoFocus - - - - - Search will continue from top/bottom if a match is not found in forward/back search + Confirm and save changes to page and margin parameters - Wrap + Change Page Layout - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + - - - - 0 - 0 - 251 - 215 - - - - Edit paragraph properties - + - :/edit-pilcrow.png:/edit-pilcrow.png + :/magnifier.png:/magnifier.png - - Paragraph Properties Editor + + Find and Replace - + + Find and replace + + - - - Qt::NoFocus - - - Unlock to edit paragraph properties - - - Lock - - + + true - - - - - - - - Paragraph - - - - - - - Paragraph number - - - 0 - - - - - - - Creation Time - - - - - - - Qt::NoFocus - - - Modifies creation time of paragraph - - - yyyy-MM-dd hh:mm:ss.zzz - - - - - - - Edit Time - - - - - - - Qt::NoFocus - - - Read only. Displays edit time -of paragraph. Will change upon -any cursor movement. - - - true - - - yyyy-MM-dd hh:mm:ss.zzz - - - - - - - Audio Start Time - - - - - - - Qt::NoFocus - - - Modifies audio start time of -paragraph. By default, the -audio end time of the previous -paragraph is set by this -unless previous paragraph has -audio end time set. - - - QDateTimeEdit::HourSection - - - hh:mm:ss.zzz - - - - - - - Audio End Time - - - - - - - Qt::NoFocus - - - Sets audio end time for this -paragraph. If not set, audio -end time is the audio start -time from next paragraph. - - - hh:mm:ss.zzz - - - - - - - Notes - - - - - - - Qt::NoFocus - - - Custom notes attached to paragraph - - - - - - - - - false - - - Qt::NoFocus - - - Enable with checkbox in order to submit edits - - - Edit Paragraph Properties - - - - - - - - - 0 - 0 - 239 - 279 - - - - - :/microphone.png:/microphone.png - - - Audio Recording - - - - - - - - - - Input Device: - - - - - - - Audio Codec: - - - - - - - File Container: - - - - - - - Sample Rate - - - - - - - Select where to receive audio input + + + + 0 + 0 + 405 + 240 + + + + + + + Search Types + + + + + Search visible text for match + + + Text + + + true + + + + + + + Search strokes, find text has to match stroke completely + + + Underlying Steno + + + + + + + Search what appears to be untranslated chords in visible text + + + Untrans + + + + - - - - Select the audio codec to record with, depends on system codecs - - + + + + + + Text to search for + + + Find + + + + + + + Perform selected search forwards + + + Next + + + + + + + Perform selected search backwards + + + Previous + + + + - - - - Select audio file type, depends on system - - + + + + + + The text to replace the match found in search + + + Replace + + + + + + + Replace the found match with the text in the replace textbox + + + Once + + + + + + + Replace all matches with text in replace textbox + + + All + + + + - - - - Select the sample rate for the recorded audio - - - QComboBox::InsertAtTop - - + + + + + + Case sensitive search + + + Match Case + + + + + + + Text in find has to match text as a whole word/stroke + + + Whole Word/Stroke + + + + + + + Search will continue from top/bottom if a match is not found in forward/back search + + + Wrap + + + + - - - - Select number of channels to record from + + + + Qt::Vertical - - - - - - Channels + + + 20 + 40 + - + - - - - - Encoding Mode - - - - - - Constant quality means varying bitrate for audio file - - - Constant Quality - - - true - - - - - - - Quality of audio recording, from very bad to very good - - - 4 - - - 2 - - - Qt::Horizontal - - - - - - - Constant bitrate means quality will vary - - - Constant Bitrate - - - - - - - Select bitrate for recording - - - - - - - + + - - - - 0 - 0 - 361 - 164 - - + - :/spell-check.png:/spell-check.png + :/edit-pilcrow.png:/edit-pilcrow.png - - Spellcheck + + Paragraph + + + Edit paragraph properties - + - - - - - - - - en-US - - - - + + + true + + + + + 0 + 0 + 405 + 240 + + + - - - Search + + + Qt::NoFocus + + + Unlock to edit paragraph properties - - - - - Skip + Lock + + + true - - - Ignore All - - + + + + + Paragraph + + + + + + + Paragraph number + + + 0 + + + + + + + Creation Time + + + + + + + Qt::NoFocus + + + Modifies creation time of paragraph + + + yyyy-MM-dd hh:mm:ss.zzz + + + + + + + Edit Time + + + + + + + Qt::NoFocus + + + Read only. Displays edit time of paragraph. Will change upon any cursor movement. + + + true + + + yyyy-MM-dd hh:mm:ss.zzz + + + + + + + Audio Start Time + + + + + + + Qt::NoFocus + + + Modifies audio start time of paragraph. + + + QDateTimeEdit::HourSection + + + hh:mm:ss.zzz + + + + + + + Audio End Time + + + + + + + Qt::NoFocus + + + Sets audio end time for this paragraph. + + + hh:mm:ss.zzz + + + + + + + Notes + + + + + + + Qt::NoFocus + + + Custom notes attached to paragraph + + + + - + + + false + + + Qt::NoFocus + + + Enable with checkbox in order to submit edits + - Change + Edit Paragraph Properties - + Qt::Vertical @@ -2366,45 +2161,314 @@ time from next paragraph. - - - + + + + + + + + + :/microphone.png:/microphone.png + + + Audio Recording + + + Set audio recording parameters + + + + + + true + + + + + 0 + 0 + 388 + 283 + + + - - - + + + - Detected: + Input Device: - - + + + + Audio Codec: + + + + + + + File Container: + + + + + + + Sample Rate + + + + + + + Select where to receive audio input + + + + + + + Select the audio codec to record with, depends on system codecs + + + + + + + Select audio file type, depends on system + + + + + + + Select the sample rate for the recorded audio + + + QComboBox::InsertAtTop + + + + + + + Select number of channels to record from + + + + + + + Channels + + - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Double click on choice to replace - - - QAbstractItemView::DoubleClicked - - - QAbstractItemView::SelectColumns + + + Encoding Mode + + + + + Constant quality means varying bitrate for audio file + + + Constant Quality + + + true + + + + + + + Quality of audio recording, from very bad to very good + + + 4 + + + 2 + + + Qt::Horizontal + + + + + + + Constant bitrate means quality will vary + + + Constant Bitrate + + + + + + + Select bitrate for recording + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - + + + + + + + + + :/spell-check.png:/spell-check.png + + + Spellcheck + + + Spellcheck editor transcript + + + + + + true + + + + + 0 + 0 + 405 + 240 + + + + + + + + + + + + en-US + + + + + + + + Search + + + + + + + Skip + + + + + + + Ignore All + + + + + + + Change + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Detected: + + + + + + + + + + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Double click on choice to replace + + + QAbstractItemView::DoubleClicked + + + QAbstractItemView::SelectColumns + + + + + + + + + + @@ -2453,7 +2517,7 @@ time from next paragraph. - 97 + 100 210 @@ -2488,7 +2552,7 @@ time from next paragraph. Qt::NoContextMenu - Placeholder for new development + Session actions for undo/redo @@ -2500,12 +2564,19 @@ time from next paragraph. - + + + Previous versions to revert to + + + + Confirm revert transcript to selected revision + Revert @@ -2521,7 +2592,7 @@ time from next paragraph. - 174 + 127 113 @@ -2620,7 +2691,7 @@ time from next paragraph. - 91 + 100 113 @@ -2804,63 +2875,6 @@ time from next paragraph. Ctrl+V - - - - :/magnifier-zoom-in.png:/magnifier-zoom-in.png - - - Zoom In - - - Ctrl+= - - - - - - :/magnifier-zoom-out.png:/magnifier-zoom-out.png - - - Zoom Out - - - Ctrl+- - - - - - - :/arrow-curve-180.png:/arrow-curve-180.png - - - Undo - - - Undo writing or other action - - - Ctrl+Z - - - Qt::WindowShortcut - - - - - - :/arrow-curve.png:/arrow-curve.png - - - Redo - - - Redo writing or other action - - - Ctrl+Y - - @@ -2967,9 +2981,7 @@ time from next paragraph. Merge Paragraphs - Merge two paragraphs by selecting across -two paragraphs, or place cursor in second of -paragraphs to merge + Merge two paragraphs by selecting across two paragraphs, or place cursor in second of paragraphs to merge @@ -2993,7 +3005,7 @@ paragraphs to merge Window Font - Set font and size + Set font and size for window @@ -3111,7 +3123,7 @@ paragraphs to merge Lock Cursor At End - All writing will be appended to end of document + Keep cursor at end of document for writing @@ -3122,7 +3134,7 @@ paragraphs to merge Capture All Steno Output - Steno output will be recorded even if window is not in focus + Record steno output even if window is not in focus @@ -3219,6 +3231,9 @@ paragraphs to merge Reset Paragraph + + Reset paragraph by clearing all data and text + @@ -3258,6 +3273,9 @@ paragraphs to merge About + + Show version information + F1 @@ -3281,6 +3299,9 @@ paragraphs to merge Paper Tape Font + + Set font for Paper Tape dock + @@ -3298,19 +3319,24 @@ paragraphs to merge Generate Style File From Template - Select template file (ODT) and generate style json -with supported formatting only. + Select template file (ODT) and generate style json with supported formatting only. Create New Style + + Create new style based on existing style + Refresh Editor + + Refresh formatting for entire editor + F5 @@ -3324,8 +3350,7 @@ with supported formatting only. Normal Text - Insert normal text through popup -dialog box at cursor location + Insert normal text through dialog Ins @@ -3342,6 +3367,9 @@ dialog box at cursor location Show All Characters + + Show all characters, including invisible ones + @@ -3378,11 +3406,17 @@ dialog box at cursor location Jump to Paragraph ... + + Jump to paragraph by paragraph number + Translate Tape + + Import tape file to translate to editor + @@ -3397,6 +3431,9 @@ dialog box at cursor location Image + + Insert image object + @@ -3407,7 +3444,7 @@ dialog box at cursor location Background Color - Set background color + Set background color of editor window @@ -3425,6 +3462,9 @@ dialog box at cursor location Delete Last Untrans + + Scan to find last untranslate, and delete it + @@ -3444,6 +3484,9 @@ dialog box at cursor location Edit Fields + + Edit fields values + @@ -3456,7 +3499,7 @@ dialog box at cursor location Autosave - Enable autosave to backup file every 5 minutes. + Enable autosave to backup file at defined time intervals @@ -3467,28 +3510,40 @@ dialog box at cursor location Edit Paragraph Affixes - Open dialog to edit paragraph affixes + Add and edit paragraph affixes for styles Set Autosave Time + + Set time intervals for autosave + Edit Menu Shortcuts + + Customize shortcuts for menu items + Edit Indices + + Add and edit index and index entries + Transcript Suggestions + + Analyze transcript for common words/n-grams to add to dictionary + diff --git a/plover_cat/qcommands.py b/plover_cat/qcommands.py index 11b0bcc..bcd67e3 100644 --- a/plover_cat/qcommands.py +++ b/plover_cat/qcommands.py @@ -127,7 +127,7 @@ def redo(self): self.document.setTextCursor(current_cursor) log_dict = {"action": "remove", "block": self.block, "position_in_block": self.position_in_block, "end": self.position_in_block + self.length} log.info(f"Remove: {log_dict}") - self.setText("Remove: %d chars" % len(self.steno)) + self.setText("Remove: %d backspace(s)" % len(self.steno)) def undo(self): current_cursor = self.document.textCursor() current_block = self.document.document().findBlockByNumber(self.block) @@ -177,7 +177,7 @@ def redo(self): log.info(f"Insert: {log_dict}") current_cursor.insertImage(imageFormat) current_block.setUserState(1) - self.setText("Insert image") + self.setText("Insert: image object") self.document.setTextCursor(current_cursor) def undo(self): current_cursor = self.document.textCursor() @@ -253,7 +253,7 @@ def redo(self): current_block.setUserData(first_data) new_block.setUserData(second_data) new_block.setUserState(1) - self.setText("Split: %d,%d" % (self.block, self.position_in_block)) + self.setText("Split: paragraph %d at %d" % (self.block, self.position_in_block)) log_dict = {"action": "split", "block": self.block, "position_in_block": self.position_in_block} log.info(f"Split: {log_dict}") self.block_state = current_block.userState() @@ -334,7 +334,7 @@ def redo(self): log_dict = {"action": "merge", "block": self.block} log.info(f"Merge: {log_dict}") self.document.setTextCursor(current_cursor) - self.setText("Merge: %d & %d" % (first_block_num, second_block_num)) + self.setText("Merge: paragraphs %d & %d" % (first_block_num, second_block_num)) def undo(self): current_cursor = self.document.textCursor() first_block_num = self.block @@ -389,7 +389,7 @@ def redo(self): self.style = list(self.par_formats.keys())[0] block_data = update_user_data(block_data, "style", self.style) current_block.setUserData(block_data) - self.setText("Style: Par. %d set style %s" % (self.block, self.style)) + self.setText(f"Format: set paragraph {self.block} style to {self.style}") current_cursor.movePosition(QTextCursor.StartOfBlock) current_cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) current_cursor.setBlockFormat(self.par_formats[self.style]) @@ -435,6 +435,26 @@ def undo(self): log_dict = {"action": "set_style", "block": self.block, "style": self.old_style} log.info(f"Style: {log_dict}") +class style_update(QUndoCommand): + def __init__(self, styles, style_name, new_style_dict): + super().__init__() + self.styles = styles + self.style_name = style_name + self.new_style_dict = deepcopy(new_style_dict) + self.old_style_dict = {} + def redo(self): + if self.style_name in self.styles: + self.old_style_dict = deepcopy(self.styles[self.style_name]) + self.styles[self.style_name] = self.new_style_dict + log_dict = {"action": "edit_style", "style_dict": self.new_style_dict} + log.info(f"Style: {log_dict}") + self.setText(f"Style: update style attributes for style {self.style_name}") + def undo(self): + if self.old_style_dict: + self.styles[self.style_name] = self.old_style_dict + log_dict = {"action": "edit_style", "style_dict": self.old_style_dict} + log.info(f"Style undo: {log_dict}") + class update_field(QUndoCommand): def __init__(self, document, block, position, old_dict, new_dict): super().__init__() @@ -470,7 +490,7 @@ def redo(self): break block = block.next() current_cursor.setPosition(current_block.position() + self.position_in_block) - self.setText("Update fields") + self.setText("Fields: update fields") log_dict = {"action": "field", "field": self.new_dict} log.info(f"Field: {log_dict}") def undo(self): @@ -530,7 +550,7 @@ def redo(self): break block = block.next() current_cursor.setPosition(current_block.position() + self.position_in_block) - self.setText("Update indexes.") + self.setText("Indices: update indices.") log_dict = {"action": "index", "index": self.new_dict} log.info(f"Index: {log_dict}") def undo(self): diff --git a/plover_cat/rtf_parsing.py b/plover_cat/rtf_parsing.py index 8ee3373..1ab3f36 100644 --- a/plover_cat/rtf_parsing.py +++ b/plover_cat/rtf_parsing.py @@ -28,14 +28,16 @@ from plover import log +from plover_cat.steno_objects import * # control chars \ { } LBRACE, RBRACE, BKS = map(Suppress, "{}\\") -text_whitespace = Word(printables, exclude_chars="\\{}") + Opt(White(" ", max=1)) +text_whitespace = Opt(White(" \t")) + Word(printables, exclude_chars="\\{}") + Opt(White(" \t")) text_whitespace.set_name("text") +text_whitespace.leave_whitespace() # special chars that exist as control words, should be replaced by actual unicode equivalents, tab_char = Literal("\\tab").set_parse_action(replace_with("\N{CHARACTER TABULATION}")) # "\t" or "\N{CHARACTER TABULATION}" @@ -97,6 +99,7 @@ def control_parse(s, l, t): Combine( Word(alphas)("control") + Opt(Word(nums + '-').set_parse_action(common.convert_to_integer))("num") + + Opt(White(" ", max=1)) + Opt(Literal(";"))("ending") ) ) @@ -226,34 +229,35 @@ def parse_cxa(self, element): cxa_dict = collapse_dict(element) self.steno = "" self.text = cxa_dict["text"] - self.append_stroke() + timestamp = "%sT%s:%s:%s.%s" % (self.date, self.timecode["hour"], self.timecode["min"], self.timecode["sec"], self.timecode["milli"]) + ael = automatic_text(prefix = cxa_dict["text"], time = timestamp) + self.par.append(ael.to_json()) def parse_steno(self, element): try: stroke = element[1]["value"] except: stroke = "" self.steno = stroke - # self.steno = stroke_element def parse_text(self, element): self.text = element["value"] def append_stroke(self): timestamp = "%sT%s:%s:%s.%s" % (self.date, self.timecode["hour"], self.timecode["min"], self.timecode["sec"], self.timecode["milli"]) - stroke = [timestamp, self.steno, self.text] - self.par.append(stroke) + ael = stroke_text(time = timestamp, stroke = self.steno, text = self.text) + self.par.append(ael.to_json()) def convert_framerate_milli(self, frames): milli = 1000 * frames / self.framerate return(milli) def set_new_paragraph(self): - strokes = self.par par_num = str(len(self.paragraphs)) - par_text = "".join([stroke[2] for stroke in self.par]) + # par_text = "".join([stroke[2] for stroke in self.par]) # this last stroke should capture the stroke emitting \par timestamp = "%sT%s:%s:%s.%s" % (self.date, self.timecode["hour"], self.timecode["min"], self.timecode["sec"], self.timecode["milli"]) - last_stroke = [timestamp, self.steno, "\n"] - self.par.append(last_stroke) + last_stroke = stroke_text(time = timestamp, stroke = self.steno, text = "\n") + self.par.append(last_stroke.to_json()) + strokes = self.par par_dict = {} - par_dict["text"] = par_text - par_dict["data"] = {"strokes": strokes, "creationtime": strokes[0][0]} + par_dict["strokes"] = strokes + par_dict["creationtime"] = strokes[0]["time"] par_dict["style"] = self.par_style self.paragraphs[par_num] = par_dict self.par = [] @@ -306,7 +310,7 @@ def scan_par_styles(self): style_list = [] for i in style_string.scanString(data): style_list.append(i[0].asList()) - print(len(style_list)) + # print(len(style_list)) par_style_index = 0 for ind, el in enumerate(style_list): new_style_dict = collapse_dict(el) @@ -360,9 +364,6 @@ def modify_styleindex_to_name(styles, style_names): styles[key] = i return(styles) -def modify_fontindex_to_name(font,font_name): - pass - def extract_par_style(style_dict): one_style_dict = {} one_part_dict = {} @@ -477,19 +478,3 @@ def load_rtf_styles(parse_results): renamed_indiv_style.append(new_style_name) return(style_dict, renamed_indiv_style) -def generate_stroke_rtf(stroke): - time_string = datetime.strptime(stroke[0], "%Y-%m-%dT%H:%M:%S.%f").strftime('%H:%M:%S') - string = write_command("cxt", time_string + ":00", visible = False, group = True) + write_command("cxs", stroke[1], visible = False, group = True) + stroke[2] - return(string) - -def write_command(control, text = None, value = None, visible = True, group = False): - command = "\\" + control - if value is not None: - command = command + str(value) - if text: - command = command + " " + text - if not visible: - command = "\\*" + command - if group: - command = "{" + command + "}" - return(command) \ No newline at end of file diff --git a/plover_cat/shortcut_dialog.ui b/plover_cat/shortcut_dialog.ui index 84ac6b0..2eb5580 100644 --- a/plover_cat/shortcut_dialog.ui +++ b/plover_cat/shortcut_dialog.ui @@ -24,7 +24,11 @@ - + + + Name of menu item + + @@ -34,7 +38,11 @@ - + + + Click and then press keys for shortcut + + @@ -45,6 +53,9 @@ Qt::NoContextMenu + + Check if shortcuts conflict and save + Validate and save diff --git a/plover_cat/shortcut_dialog_ui.py b/plover_cat/shortcut_dialog_ui.py deleted file mode 100644 index eddd4a9..0000000 --- a/plover_cat/shortcut_dialog_ui.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'plover_cat\shortcut_dialog.ui' -# -# Created by: PyQt5 UI code generator 5.15.9 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_shortcutDialog(object): - def setupUi(self, shortcutDialog): - shortcutDialog.setObjectName("shortcutDialog") - shortcutDialog.resize(391, 129) - self.verticalLayout = QtWidgets.QVBoxLayout(shortcutDialog) - self.verticalLayout.setObjectName("verticalLayout") - self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setObjectName("formLayout") - self.label = QtWidgets.QLabel(shortcutDialog) - self.label.setObjectName("label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) - self.text_name = QtWidgets.QComboBox(shortcutDialog) - self.text_name.setObjectName("text_name") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.text_name) - self.label_2 = QtWidgets.QLabel(shortcutDialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) - self.shortcut = QtWidgets.QKeySequenceEdit(shortcutDialog) - self.shortcut.setObjectName("shortcut") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.shortcut) - self.verticalLayout.addLayout(self.formLayout) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.validate = QtWidgets.QPushButton(shortcutDialog) - self.validate.setContextMenuPolicy(QtCore.Qt.NoContextMenu) - self.validate.setObjectName("validate") - self.horizontalLayout.addWidget(self.validate) - self.buttonBox = QtWidgets.QDialogButtonBox(shortcutDialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.buttonBox.sizePolicy().hasHeightForWidth()) - self.buttonBox.setSizePolicy(sizePolicy) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout.addWidget(self.buttonBox) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(shortcutDialog) - self.buttonBox.accepted.connect(shortcutDialog.accept) # type: ignore - self.buttonBox.rejected.connect(shortcutDialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(shortcutDialog) - - def retranslateUi(self, shortcutDialog): - _translate = QtCore.QCoreApplication.translate - shortcutDialog.setWindowTitle(_translate("shortcutDialog", "Dialog")) - self.label.setText(_translate("shortcutDialog", "Menu Item")) - self.label_2.setText(_translate("shortcutDialog", "Shortcut")) - self.validate.setText(_translate("shortcutDialog", "Validate and save")) diff --git a/plover_cat/steno_objects.py b/plover_cat/steno_objects.py index de0996d..e66ad38 100644 --- a/plover_cat/steno_objects.py +++ b/plover_cat/steno_objects.py @@ -5,8 +5,7 @@ from copy import deepcopy from itertools import accumulate from bisect import bisect_left, bisect -from plover_cat.rtf_parsing import write_command -from plover_cat.helpers import pixel_to_in +from plover_cat.helpers import pixel_to_in, write_command from plover_cat.constants import user_field_dict from PyQt5.QtCore import QByteArray, QBuffer, QIODevice from PyQt5.QtGui import QImage, QImageReader @@ -14,6 +13,10 @@ from odf.text import P, UserFieldDecls, UserFieldDecl, UserFieldGet, UserIndexMarkStart, UserIndexMarkEnd from odf.draw import Frame, TextBox, Image +_whitespace = '\t\n\x0b\x0c\r ' +whitespace = r'[%s]' % re.escape(_whitespace) +wordsep_simple_re = re.compile(r'(%s+)' % whitespace) + # display letters: s for stroke, t for text, i for image, # sa for auto, c for conflict, f for field # x for "index"/references, e for exhibit @@ -28,6 +31,22 @@ def __init__(self, text = "", time = None): self.time = time or datetime.now().isoformat("T", "milliseconds") def __len__(self): return(len(self.data)) + def split(self): + if self.length() > 1: + chunks = wordsep_simple_re.split(self.data) + list_chunks = [] + for c in chunks: + class_dict = deepcopy(self.__dict__) + class_dict["data"] = c + new_element = self.__class__() + new_element.from_dict(class_dict) + list_chunks.append(new_element) + return(list_chunks) + else: + class_dict = deepcopy(self.__dict__) + new_element = self.__class__() + new_element.from_dict(class_dict) + return([new_element]) def __getitem__(self, key): class_dict = deepcopy(self.__dict__) class_dict["data"] = self.data[key] @@ -170,9 +189,7 @@ def to_odt(self, paragraph, document): paragraph.addElement(user_field) class automatic_text(stroke_text): - """ - use for text such as Q\t, ie set directly for question style - """ + """use for text such as Q\t, ie set directly for question style""" def __init__(self, prefix = "", suffix = "", **kargs): super().__init__(**kargs) self.element = "automatic" @@ -533,6 +550,12 @@ def search_text(self, query): def collection_time(self, reverse = False): times = [el.time for el in self.data] return(sorted(times, reverse = reverse)[0]) + def audio_time(self, reverse = False): + times = [el.audiotime for el in self.data if el.element == "stroke" and el.audiotime != ""] + if times: + return(sorted(times, reverse = reverse)[0]) + else: + return None def replace_initial_tab(self, tab_replace = " "): track_len = 3 for el in self.data: @@ -580,25 +603,10 @@ def __init__(self, **kargs): def _split(self, text): # override - # split text/stroke elements, return as chunked text elements - # other elements just keep together text.remove_end() chunks = [] - txt = "" for el in text.data: - if el.element in ["stroke", "text"]: - # keep adding to string as long as it is still "text" - txt = txt + el.to_text() - else: - # add txt string and then append element, so list is ["text", el, "text"] - txt_chunks = self.wordsep_simple_re.split(txt) - chunks.extend([text_element(i) for i in txt_chunks if i]) - txt = "" - chunks.append(el) - if txt: - # if there is remaining text, append - txt_chunks = self.wordsep_simple_re.split(txt) - chunks.extend([text_element(i) for i in txt_chunks if i]) + chunks.extend(el.split()) return chunks def _wrap_chunks(self, chunks): diff --git a/plover_cat/suggest_dialog.ui b/plover_cat/suggest_dialog.ui index 95a3dbc..765d334 100644 --- a/plover_cat/suggest_dialog.ui +++ b/plover_cat/suggest_dialog.ui @@ -46,6 +46,9 @@ + + Filter for words with size greater than selected in SCOWL. If blank, only filters common stopwords + 0 @@ -110,6 +113,9 @@ + + Word/n-gram must occur at least this many times + 1 @@ -127,6 +133,9 @@ + + N-gram must be this many words long + 2 @@ -141,6 +150,9 @@ + + N-gram cannot be longer than this value + 2 @@ -155,6 +167,9 @@ + + Perform selected search + Detect @@ -169,6 +184,9 @@ + + Send selected translation and outline to dictionary + To Dictionary