Completely redesigned the indentation plugin. Now adds a tab to preferences with complete customization for how indentation is presented.

This commit is contained in:
Storm Dragon
2026-01-03 15:13:48 -05:00
parent e32db0d2c9
commit c1ede7728e
11 changed files with 1366 additions and 114 deletions
+670 -21
View File
@@ -21,6 +21,55 @@
<property name="step_increment">10</property>
<property name="page_increment">50</property>
</object>
<object class="GtkAdjustment" id="indentationSpacesPerLevelAdjustment">
<property name="lower">1</property>
<property name="upper">16</property>
<property name="value">4</property>
<property name="step_increment">1</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="indentationTabWidthAdjustment">
<property name="lower">1</property>
<property name="upper">16</property>
<property name="value">4</property>
<property name="step_increment">1</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="indentationAudioBaseFrequencyAdjustment">
<property name="lower">50</property>
<property name="upper">2000</property>
<property name="value">200</property>
<property name="step_increment">10</property>
<property name="page_increment">50</property>
</object>
<object class="GtkAdjustment" id="indentationAudioStepFrequencyAdjustment">
<property name="lower">1</property>
<property name="upper">500</property>
<property name="value">80</property>
<property name="step_increment">5</property>
<property name="page_increment">20</property>
</object>
<object class="GtkAdjustment" id="indentationAudioMaxFrequencyAdjustment">
<property name="lower">200</property>
<property name="upper">5000</property>
<property name="value">1200</property>
<property name="step_increment">50</property>
<property name="page_increment">100</property>
</object>
<object class="GtkAdjustment" id="indentationAudioDurationAdjustment">
<property name="lower">0.01</property>
<property name="upper">2</property>
<property name="value">0.15</property>
<property name="step_increment">0.01</property>
<property name="page_increment">0.1</property>
</object>
<object class="GtkAdjustment" id="indentationAudioVolumeAdjustment">
<property name="lower">0</property>
<property name="upper">1</property>
<property name="value">0.7</property>
<property name="step_increment">0.05</property>
<property name="page_increment">0.1</property>
</object>
<object class="GtkListStore" id="liststore1">
<columns>
<!-- column-name gchararray1 -->
@@ -1830,21 +1879,6 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="enableSpeechIndentationCheckButton">
<property name="label" translatable="yes">Speak _indentation and justification</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="enableMnemonicSpeakingCheckButton">
<property name="label" translatable="yes">Spea_k object mnemonics</property>
@@ -3331,8 +3365,8 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="textAttributesConfigGrid">
<child>
<object class="GtkGrid" id="textAttributesConfigGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
@@ -3553,6 +3587,621 @@
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="indentationGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="row_spacing">10</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkFrame" id="indentationPresentationFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="indentationPresentationAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="indentationPresentationGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<child>
<object class="GtkRadioButton" id="indentationPresentationOffButton">
<property name="label" translatable="yes">_Off</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="indentationPresentationModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationPresentationSpeechButton">
<property name="label" translatable="yes">_Speech</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationPresentationOffButton</property>
<signal name="toggled" handler="indentationPresentationModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationPresentationBeepsButton">
<property name="label" translatable="yes">_Beeps</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationPresentationOffButton</property>
<signal name="toggled" handler="indentationPresentationModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationPresentationSpeechAndBeepsButton">
<property name="label" translatable="yes">Speech _and beeps</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationPresentationOffButton</property>
<signal name="toggled" handler="indentationPresentationModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="indentationPresentationLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Presentation Mode</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="indentationChangeFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="indentationChangeAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="indentationChangeGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<child>
<object class="GtkRadioButton" id="indentationChangeAlwaysButton">
<property name="label" translatable="yes">Always present indentation</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="indentationChangeModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationChangeAnyButton">
<property name="label" translatable="yes">Present any indentation changes</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationChangeAlwaysButton</property>
<signal name="toggled" handler="indentationChangeModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationChangeLevelButton">
<property name="label" translatable="yes">Present only when indentation level changes</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationChangeAlwaysButton</property>
<signal name="toggled" handler="indentationChangeModeToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="indentationChangeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">When to Present</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="indentationSpeechFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="indentationSpeechAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="indentationSpeechGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<child>
<object class="GtkRadioButton" id="indentationSpeechSpacesTabsButton">
<property name="label" translatable="yes">Spaces and tabs</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="indentationSpeechStyleToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationSpeechLevelsButton">
<property name="label" translatable="yes">Indentation level</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationSpeechSpacesTabsButton</property>
<signal name="toggled" handler="indentationSpeechStyleToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationSpeechColumnsButton">
<property name="label" translatable="yes">Indentation columns</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationSpeechSpacesTabsButton</property>
<signal name="toggled" handler="indentationSpeechStyleToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="indentationSpeechLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Speech Format</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="indentationUnitsFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="indentationUnitsAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="indentationUnitsGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkLabel" id="indentationSpacesPerLevelLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Spaces per level:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationSpacesPerLevelSpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationSpacesPerLevelSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationSpacesPerLevelAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationTabWidthLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Tab width:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationTabWidthSpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationTabWidthSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationTabWidthAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationAudioUnitLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Audio unit:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="indentationAudioUnitGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">4</property>
<child>
<object class="GtkRadioButton" id="indentationAudioUnitLevelsButton">
<property name="label" translatable="yes">Levels</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="indentationAudioUnitToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="indentationAudioUnitColumnsButton">
<property name="label" translatable="yes">Columns</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<property name="group">indentationAudioUnitLevelsButton</property>
<signal name="toggled" handler="indentationAudioUnitToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="indentationUnitsLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Units</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="indentationAudioFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="indentationAudioAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="indentationAudioGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkLabel" id="indentationAudioBaseFrequencyLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Base frequency (Hz):</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationAudioBaseFrequencySpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationAudioBaseFrequencySpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationAudioBaseFrequencyAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationAudioStepFrequencyLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Step per unit (Hz):</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationAudioStepFrequencySpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationAudioStepFrequencySpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationAudioStepFrequencyAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationAudioMaxFrequencyLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Maximum frequency (Hz):</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationAudioMaxFrequencySpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationAudioMaxFrequencySpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationAudioMaxFrequencyAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationAudioDurationLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Tone duration (sec):</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationAudioDurationSpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationAudioDurationSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationAudioDurationAdjustment</property>
<property name="climb_rate">0.01</property>
<property name="digits">2</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="indentationAudioVolumeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Volume multiplier:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">indentationAudioVolumeSpinButton</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="indentationAudioVolumeSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">indentationAudioVolumeAdjustment</property>
<property name="climb_rate">0.05</property>
<property name="digits">2</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="indentationSpinValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="indentationAudioLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Audio</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">8</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="indentationTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Indentation</property>
</object>
<packing>
<property name="position">8</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="aiPage">
<property name="visible">True</property>
@@ -3773,7 +4422,7 @@
</child>
</object>
<packing>
<property name="position">8</property>
<property name="position">9</property>
</packing>
</child>
<child type="tab">
@@ -3783,7 +4432,7 @@
<property name="label" translatable="yes">AI Assistant</property>
</object>
<packing>
<property name="position">8</property>
<property name="position">9</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -3959,7 +4608,7 @@
</child>
</object>
<packing>
<property name="position">9</property>
<property name="position">10</property>
</packing>
</child>
<child type="tab">
@@ -3969,7 +4618,7 @@
<property name="label" translatable="yes">OCR</property>
</object>
<packing>
<property name="position">9</property>
<property name="position">10</property>
<property name="tab_fill">False</property>
</packing>
</child>
+203 -2
View File
@@ -1482,8 +1482,6 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("onlySpeakDisplayedTextCheckButton").set_active(
prefs["onlySpeakDisplayedText"])
self.get_widget("enableSpeechIndentationCheckButton").set_active(\
prefs["enableSpeechIndentation"])
self.get_widget("speakBlankLinesCheckButton").set_active(\
prefs["speakBlankLines"])
@@ -1825,6 +1823,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# AI Assistant settings
#
self._initAIState()
# Indentation settings
#
self._initIndentationState()
# OCR Plugin settings
#
@@ -1929,6 +1931,119 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if enabled:
current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
self._updateProviderControls(current_provider)
def _initIndentationState(self):
"""Initialize Indentation tab widgets with current settings."""
prefs = self.prefsDict
self.indentationPresentationOffButton = self.get_widget("indentationPresentationOffButton")
self.indentationPresentationSpeechButton = self.get_widget("indentationPresentationSpeechButton")
self.indentationPresentationBeepsButton = self.get_widget("indentationPresentationBeepsButton")
self.indentationPresentationSpeechAndBeepsButton = self.get_widget("indentationPresentationSpeechAndBeepsButton")
self.indentationChangeAlwaysButton = self.get_widget("indentationChangeAlwaysButton")
self.indentationChangeAnyButton = self.get_widget("indentationChangeAnyButton")
self.indentationChangeLevelButton = self.get_widget("indentationChangeLevelButton")
self.indentationSpeechSpacesTabsButton = self.get_widget("indentationSpeechSpacesTabsButton")
self.indentationSpeechLevelsButton = self.get_widget("indentationSpeechLevelsButton")
self.indentationSpeechColumnsButton = self.get_widget("indentationSpeechColumnsButton")
self.indentationAudioUnitLevelsButton = self.get_widget("indentationAudioUnitLevelsButton")
self.indentationAudioUnitColumnsButton = self.get_widget("indentationAudioUnitColumnsButton")
self.indentationSpacesPerLevelSpinButton = self.get_widget("indentationSpacesPerLevelSpinButton")
self.indentationTabWidthSpinButton = self.get_widget("indentationTabWidthSpinButton")
self.indentationAudioBaseFrequencySpinButton = self.get_widget("indentationAudioBaseFrequencySpinButton")
self.indentationAudioStepFrequencySpinButton = self.get_widget("indentationAudioStepFrequencySpinButton")
self.indentationAudioMaxFrequencySpinButton = self.get_widget("indentationAudioMaxFrequencySpinButton")
self.indentationAudioDurationSpinButton = self.get_widget("indentationAudioDurationSpinButton")
self.indentationAudioVolumeSpinButton = self.get_widget("indentationAudioVolumeSpinButton")
mode = prefs.get("indentationPresentationMode", settings.indentationPresentationMode)
if mode == settings.INDENTATION_PRESENTATION_SPEECH:
self.indentationPresentationSpeechButton.set_active(True)
elif mode == settings.INDENTATION_PRESENTATION_BEEPS:
self.indentationPresentationBeepsButton.set_active(True)
elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
self.indentationPresentationSpeechAndBeepsButton.set_active(True)
else:
self.indentationPresentationOffButton.set_active(True)
enableSpeechIndentation = mode in (
settings.INDENTATION_PRESENTATION_SPEECH,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
self.prefsDict["enableSpeechIndentation"] = enableSpeechIndentation
only_if_changed = prefs.get("speakIndentationOnlyIfChanged", settings.speakIndentationOnlyIfChanged)
change_mode = prefs.get("indentationChangeMode", settings.indentationChangeMode)
if not only_if_changed or change_mode == settings.INDENTATION_CHANGE_ALWAYS:
self.indentationChangeAlwaysButton.set_active(True)
elif change_mode == settings.INDENTATION_CHANGE_ANY:
self.indentationChangeAnyButton.set_active(True)
else:
self.indentationChangeLevelButton.set_active(True)
speech_style = prefs.get("indentationSpeechStyle", settings.indentationSpeechStyle)
if speech_style == settings.INDENTATION_SPEECH_STYLE_LEVELS:
self.indentationSpeechLevelsButton.set_active(True)
elif speech_style == settings.INDENTATION_SPEECH_STYLE_COLUMNS:
self.indentationSpeechColumnsButton.set_active(True)
else:
self.indentationSpeechSpacesTabsButton.set_active(True)
audio_unit = prefs.get("indentationAudioUnit", settings.indentationAudioUnit)
if audio_unit == settings.INDENTATION_UNIT_COLUMNS:
self.indentationAudioUnitColumnsButton.set_active(True)
else:
self.indentationAudioUnitLevelsButton.set_active(True)
self.indentationSpacesPerLevelSpinButton.set_value(
prefs.get("indentationSpacesPerLevel", settings.indentationSpacesPerLevel))
self.indentationTabWidthSpinButton.set_value(
prefs.get("indentationTabWidth", settings.indentationTabWidth))
self.indentationAudioBaseFrequencySpinButton.set_value(
prefs.get("indentationAudioBaseFrequency", settings.indentationAudioBaseFrequency))
self.indentationAudioStepFrequencySpinButton.set_value(
prefs.get("indentationAudioStepFrequency", settings.indentationAudioStepFrequency))
self.indentationAudioMaxFrequencySpinButton.set_value(
prefs.get("indentationAudioMaxFrequency", settings.indentationAudioMaxFrequency))
self.indentationAudioDurationSpinButton.set_value(
prefs.get("indentationAudioDuration", settings.indentationAudioDuration))
self.indentationAudioVolumeSpinButton.set_value(
prefs.get("indentationAudioVolume", settings.indentationAudioVolume))
self._updateIndentationControlsState(mode)
def _updateIndentationControlsState(self, mode):
"""Enable or disable indentation controls based on presentation mode."""
speech_enabled = mode in (
settings.INDENTATION_PRESENTATION_SPEECH,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
beeps_enabled = mode in (
settings.INDENTATION_PRESENTATION_BEEPS,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
for widget in (
self.indentationSpeechSpacesTabsButton,
self.indentationSpeechLevelsButton,
self.indentationSpeechColumnsButton,
):
widget.set_sensitive(speech_enabled)
for widget in (
self.indentationAudioUnitLevelsButton,
self.indentationAudioUnitColumnsButton,
self.indentationAudioBaseFrequencySpinButton,
self.indentationAudioStepFrequencySpinButton,
self.indentationAudioMaxFrequencySpinButton,
self.indentationAudioDurationSpinButton,
self.indentationAudioVolumeSpinButton,
):
widget.set_sensitive(beeps_enabled)
def _updateProviderControls(self, provider):
"""Update visibility/sensitivity of provider-specific controls."""
@@ -3839,6 +3954,92 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.__initProfileCombo()
# Indentation signal handlers
def indentationPresentationModeToggled(self, widget):
"""Indentation presentation mode radio toggled handler."""
if not widget.get_active():
return
mapping = {
"indentationPresentationOffButton": settings.INDENTATION_PRESENTATION_OFF,
"indentationPresentationSpeechButton": settings.INDENTATION_PRESENTATION_SPEECH,
"indentationPresentationBeepsButton": settings.INDENTATION_PRESENTATION_BEEPS,
"indentationPresentationSpeechAndBeepsButton": settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
}
widget_name = Gtk.Buildable.get_name(widget)
mode = mapping.get(widget_name)
if mode is None:
return
self.prefsDict["indentationPresentationMode"] = mode
self.prefsDict["enableSpeechIndentation"] = mode in (
settings.INDENTATION_PRESENTATION_SPEECH,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
self._updateIndentationControlsState(mode)
def indentationChangeModeToggled(self, widget):
"""Indentation change mode radio toggled handler."""
if not widget.get_active():
return
widget_name = Gtk.Buildable.get_name(widget)
if widget_name == "indentationChangeAlwaysButton":
self.prefsDict["speakIndentationOnlyIfChanged"] = False
self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_ALWAYS
elif widget_name == "indentationChangeAnyButton":
self.prefsDict["speakIndentationOnlyIfChanged"] = True
self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_ANY
else:
self.prefsDict["speakIndentationOnlyIfChanged"] = True
self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_LEVEL
def indentationSpeechStyleToggled(self, widget):
"""Indentation speech style radio toggled handler."""
if not widget.get_active():
return
mapping = {
"indentationSpeechSpacesTabsButton": settings.INDENTATION_SPEECH_STYLE_SPACES_TABS,
"indentationSpeechLevelsButton": settings.INDENTATION_SPEECH_STYLE_LEVELS,
"indentationSpeechColumnsButton": settings.INDENTATION_SPEECH_STYLE_COLUMNS,
}
widget_name = Gtk.Buildable.get_name(widget)
style = mapping.get(widget_name)
if style is None:
return
self.prefsDict["indentationSpeechStyle"] = style
def indentationAudioUnitToggled(self, widget):
"""Indentation audio unit radio toggled handler."""
if not widget.get_active():
return
if Gtk.Buildable.get_name(widget) == "indentationAudioUnitColumnsButton":
self.prefsDict["indentationAudioUnit"] = settings.INDENTATION_UNIT_COLUMNS
else:
self.prefsDict["indentationAudioUnit"] = settings.INDENTATION_UNIT_LEVELS
def indentationSpinValueChanged(self, widget):
"""Indentation spin button value changed handler."""
settingName = Gtk.Buildable.get_name(widget)
if settingName.endswith("SpinButton"):
settingName = settingName[:-10]
int_settings = {
"indentationSpacesPerLevel",
"indentationTabWidth",
"indentationAudioBaseFrequency",
"indentationAudioStepFrequency",
"indentationAudioMaxFrequency",
}
if settingName in int_settings:
self.prefsDict[settingName] = int(widget.get_value())
else:
self.prefsDict[settingName] = float(widget.get_value())
# AI Assistant signal handlers
def enableAIToggled(self, widget):
+42
View File
@@ -44,6 +44,12 @@ from typing import TYPE_CHECKING, Optional
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
try:
gi.require_version("Wnck", "3.0")
from gi.repository import Wnck
_wnck_available = True
except Exception:
_wnck_available = False
from . import debug
from . import focus_manager
@@ -53,6 +59,7 @@ from . import settings
from . import cthulhu_state
from .ax_object import AXObject
from .ax_utilities import AXUtilities
from .ax_utilities_application import AXUtilitiesApplication
if TYPE_CHECKING:
from . import keybindings
@@ -69,6 +76,35 @@ class InputEventManager:
self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
self._paused: bool = False
def _active_window_has_accessible_app(self) -> Optional[bool]:
"""Returns True if the WM active window maps to an AT-SPI app."""
if not _wnck_available:
return None
screen = Wnck.Screen.get_default()
if not screen:
return None
try:
screen.force_update()
active_window = screen.get_active_window()
except Exception:
return None
if not active_window:
return None
try:
pid = active_window.get_pid()
except Exception:
return None
if pid <= 0:
return None
return AXUtilitiesApplication.get_application_with_pid(pid) is not None
def start_key_watcher(self) -> None:
"""Starts the watcher for keyboard input events."""
@@ -281,6 +317,12 @@ class InputEventManager:
manager = focus_manager.get_manager()
if pressed:
has_accessible_app = self._active_window_has_accessible_app()
if has_accessible_app is False and manager.get_active_window() is not None:
msg = "INPUT EVENT MANAGER: Active window has no AT-SPI app. Clearing active window."
debug.print_message(debug.LEVEL_INFO, msg, True)
manager.set_active_window(None, notify_script=True)
window = manager.get_active_window()
if not AXUtilities.can_be_active_window(window):
new_window = AXUtilities.find_active_window()
+10
View File
@@ -2812,6 +2812,16 @@ def tabsCount(count):
# tab characters in a string.
return ngettext("%d tab", "%d tabs", count) % count
def indentationLevelCount(count):
# Translators: This message is presented to inform the user of the
# indentation level for a line of text.
return ngettext("indentation level %d", "indentation level %d", count) % count
def indentationColumnCount(count):
# Translators: This message is presented to inform the user of the number
# of indentation columns for a line of text.
return ngettext("indentation %d column", "indentation %d columns", count) % count
def tableCount(count, onlyIfFound=True):
if not count and onlyIfFound:
return ""
+173 -72
View File
@@ -10,11 +10,14 @@
"""IndentationAudio plugin for Cthulhu - Provides audio feedback for indentation level changes."""
import logging
import math
import re
from gi.repository import GLib
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug
from cthulhu import settings
from cthulhu import settings_manager
from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText
@@ -28,6 +31,7 @@ except ImportError as e:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sound import failed: {e}", True)
logger = logging.getLogger(__name__)
_settingsManager = settings_manager.getManager()
class IndentationAudio(Plugin):
@@ -37,14 +41,18 @@ class IndentationAudio(Plugin):
"""Initialize the IndentationAudio plugin."""
super().__init__(*args, **kwargs)
self._enabled = True # Start enabled by default
self._last_indentation_level = {} # Track per-object indentation
self._last_indentation_data = {} # Track per-object indentation
self._event_listener_id = None
self._kb_binding = None
# Audio settings
self._base_frequency = 200 # Base frequency in Hz
self._frequency_step = 80 # Hz per indentation level
self._tone_duration = 0.15 # Seconds
self._max_frequency = 1200 # Cap frequency to avoid harsh sounds
self._base_frequency = settings.indentationAudioBaseFrequency
self._frequency_step = settings.indentationAudioStepFrequency
self._tone_duration = settings.indentationAudioDuration
self._max_frequency = settings.indentationAudioMaxFrequency
self._volume_multiplier = settings.indentationAudioVolume
self._alerts_suspended = False
self._saved_presentation_mode = None
self._saved_speech_indentation = None
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True)
@@ -98,7 +106,7 @@ class IndentationAudio(Plugin):
self._disconnect_from_events()
# Clear tracking data
self._last_indentation_level.clear()
self._last_indentation_data.clear()
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True)
@@ -117,7 +125,7 @@ class IndentationAudio(Plugin):
# Register Cthulhu+I keybinding using the plugin's registerGestureByString method
gesture_string = "kb:cthulhu+i"
description = "Toggle indentation audio feedback"
description = "Toggle indentation alerts"
self._kb_binding = self.registerGestureByString(
self._toggle_indentation_audio,
@@ -169,7 +177,7 @@ class IndentationAudio(Plugin):
def _on_caret_moved(self, event):
"""Handle caret movement events."""
try:
if not self._enabled:
if not self._beeps_enabled():
return
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Caret moved in {event.source}", True)
@@ -292,18 +300,41 @@ class IndentationAudio(Plugin):
def _toggle_indentation_audio(self, script, inputEvent=None):
"""Toggle the indentation audio feedback on/off."""
try:
self._enabled = not self._enabled
state = "enabled" if self._enabled else "disabled"
current_mode = _settingsManager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
current_speech = _settingsManager.getSetting('enableSpeechIndentation')
if self._alerts_suspended:
new_mode = self._saved_presentation_mode
if new_mode is None:
new_mode = current_mode
_settingsManager.setSetting('indentationPresentationMode', new_mode)
if self._saved_speech_indentation is None:
self._sync_speech_setting(new_mode)
else:
_settingsManager.setSetting('enableSpeechIndentation', self._saved_speech_indentation)
self._alerts_suspended = False
state = "enabled"
else:
self._saved_presentation_mode = current_mode
self._saved_speech_indentation = current_speech
_settingsManager.setSetting(
'indentationPresentationMode',
settings.INDENTATION_PRESENTATION_OFF,
)
_settingsManager.setSetting('enableSpeechIndentation', False)
self._alerts_suspended = True
state = "disabled"
# Announce the state change
message = f"Indentation audio {state}"
message = f"Indentation alerts {state}"
if hasattr(script, 'speakMessage'):
script.speakMessage(message)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Toggled to {state}", True)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Alerts toggled to {state}", True)
# Test the indentation detection on current line when enabled
if self._enabled and script:
if state == "enabled" and script:
try:
# Try to get current focus object and line text
from cthulhu import cthulhu_state
@@ -324,62 +355,107 @@ class IndentationAudio(Plugin):
logger.error(f"IndentationAudio: Error toggling state: {e}")
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error toggling: {e}", True)
return False
def _calculate_indentation_level(self, line_text):
"""Calculate the indentation level of a line."""
def _sync_speech_setting(self, presentation_mode):
enable_speech = presentation_mode in (
settings.INDENTATION_PRESENTATION_SPEECH,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
_settingsManager.setSetting('enableSpeechIndentation', enable_speech)
def _beeps_enabled(self):
if not self._enabled:
return False
presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
return presentation_mode in (
settings.INDENTATION_PRESENTATION_BEEPS,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
@staticmethod
def _extract_indentation(line_text):
if not line_text:
return 0
# Remove non-breaking spaces and convert to regular spaces
return ""
line = line_text.replace("\u00a0", " ")
# Find the first non-whitespace character
match = re.search(r"[^ \t]", line)
if not match:
return 0 # Empty or whitespace-only line
indent_text = line[:match.start()]
# Calculate indentation level (4 spaces = 1 level, 1 tab = 1 level)
level = 0
for char in indent_text:
if char == '\t':
level += 1
elif char == ' ':
level += 0.25 # 4 spaces = 1 level
return int(level)
return line
return line[:match.start()]
@staticmethod
def _count_columns(indentation, tab_width):
columns = 0
tab_width = max(1, tab_width)
for char in indentation:
if char == "\t":
columns += tab_width - (columns % tab_width)
else:
columns += 1
return columns
@staticmethod
def _count_levels(columns, spaces_per_level):
spaces_per_level = max(1, spaces_per_level)
if columns <= 0:
return 0
return int(math.ceil(columns / spaces_per_level))
def _get_indentation_data(self, line_text):
indentation = self._extract_indentation(line_text)
tab_width = _settingsManager.getSetting('indentationTabWidth') \
or settings.indentationTabWidth
spaces_per_level = _settingsManager.getSetting('indentationSpacesPerLevel') \
or settings.indentationSpacesPerLevel
columns = self._count_columns(indentation, tab_width)
levels = self._count_levels(columns, spaces_per_level)
return indentation, columns, levels
def _generate_indentation_tone(self, new_level, old_level):
"""Generate an audio tone for indentation level change."""
if not self._enabled:
def _generate_indentation_tone(self, new_units, old_units):
"""Generate an audio tone for indentation change."""
if not self._beeps_enabled():
return
base_frequency = _settingsManager.getSetting('indentationAudioBaseFrequency') \
or self._base_frequency
frequency_step = _settingsManager.getSetting('indentationAudioStepFrequency') \
or self._frequency_step
max_frequency = _settingsManager.getSetting('indentationAudioMaxFrequency') \
or self._max_frequency
tone_duration = _settingsManager.getSetting('indentationAudioDuration') \
or self._tone_duration
volume_multiplier = _settingsManager.getSetting('indentationAudioVolume') \
or self._volume_multiplier
# Calculate frequency based on new indentation level
# Calculate frequency based on new indentation units
base_frequency = min(
self._base_frequency + (new_level * self._frequency_step),
self._max_frequency
base_frequency + (new_units * frequency_step),
max_frequency
)
# Add directional audio cues
if new_level > old_level:
if new_units > old_units:
# Indentation increased - higher pitch
frequency = base_frequency + 50
elif new_level < old_level:
# Indentation decreased - lower pitch
elif new_units < old_units:
# Indentation decreased - lower pitch
frequency = max(base_frequency - 50, 100)
else:
# Same level (shouldn't happen but just in case)
frequency = base_frequency
try:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: About to generate tone for level {new_level} (freq: {frequency}Hz)", True)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: About to generate tone for units {new_units} (freq: {frequency}Hz)", True)
if not SOUND_AVAILABLE or not self._player:
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound player not available, using fallback", True)
# Fallback to ASCII bell
if new_level > 0:
beeps = min(new_level, 5)
if new_units > 0:
beeps = min(new_units, 5)
for i in range(beeps):
print("\a", end="", flush=True)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sent {beeps} ASCII bell beeps", True)
@@ -388,8 +464,7 @@ class IndentationAudio(Plugin):
# Use Cthulhu's proper sound system
try:
# Create a tone based on indentation level
duration = self._tone_duration
volume_multiplier = 0.7
duration = tone_duration
tone = Tone(
duration=duration,
@@ -400,13 +475,13 @@ class IndentationAudio(Plugin):
# Play the tone
self._player.play(tone, interrupt=False)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Played Cthulhu tone - Level: {new_level}, Freq: {frequency}Hz", True)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Played Cthulhu tone - Units: {new_units}, Freq: {frequency}Hz", True)
except Exception as sound_e:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Cthulhu sound failed: {sound_e}", True)
# Fallback to ASCII bell
if new_level > 0:
beeps = min(new_level, 5)
if new_units > 0:
beeps = min(new_units, 5)
for i in range(beeps):
print("\a", end="", flush=True)
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Used fallback ASCII bell ({beeps} beeps)", True)
@@ -420,31 +495,57 @@ class IndentationAudio(Plugin):
This method is intended to be called by scripts during line navigation.
"""
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: enabled={self._enabled}, line='{line_text}'", True)
if not self._enabled or not line_text:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: line='{line_text}'", True)
if not line_text or not self._beeps_enabled():
return
try:
# Get object identifier for tracking
obj_id = str(obj) if obj else "unknown"
# Calculate current indentation level
current_level = self._calculate_indentation_level(line_text)
# Get previous level for this object
previous_level = self._last_indentation_level.get(obj_id, current_level)
# Update tracking
self._last_indentation_level[obj_id] = current_level
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Levels - previous: {previous_level}, current: {current_level}", True)
# Calculate current indentation data
indentation, columns, levels = self._get_indentation_data(line_text)
audio_unit = _settingsManager.getSetting('indentationAudioUnit') \
or settings.indentationAudioUnit
current_units = columns if audio_unit == settings.INDENTATION_UNIT_COLUMNS else levels
previous_data = self._last_indentation_data.get(obj_id)
previous_units = previous_data.get("units") if previous_data else current_units
self._last_indentation_data[obj_id] = {
"signature": indentation,
"levels": levels,
"columns": columns,
"units": current_units,
}
change_mode = _settingsManager.getSetting('indentationChangeMode') \
or settings.indentationChangeMode
only_if_changed = _settingsManager.getSetting('speakIndentationOnlyIfChanged')
if not only_if_changed:
changed = True
elif previous_data is None:
changed = True
elif change_mode == settings.INDENTATION_CHANGE_ANY:
changed = previous_data.get("signature") != indentation
elif change_mode == settings.INDENTATION_CHANGE_LEVEL:
changed = previous_data.get("levels") != levels
else:
changed = True
debug.printMessage(
debug.LEVEL_INFO,
f"IndentationAudio: Units - previous: {previous_units}, current: {current_units}, changed={changed}",
True,
)
# Play audio cue if indentation changed
if current_level != previous_level:
self._generate_indentation_tone(current_level, previous_level)
if changed:
self._generate_indentation_tone(current_units, previous_units)
debug_msg = f"IndentationAudio: Indentation changed from {previous_level} to {current_level}"
debug_msg = f"IndentationAudio: Indentation units changed from {previous_units} to {current_units}"
debug.printMessage(debug.LEVEL_INFO, debug_msg, True)
except Exception as e:
@@ -469,7 +570,7 @@ class IndentationAudio(Plugin):
self._monkey_patch_script_methods()
# Clear tracking data for new context
self._last_indentation_level.clear()
self._last_indentation_data.clear()
logger.info("IndentationAudio: Handled script change")
+177 -10
View File
@@ -118,6 +118,7 @@ class Utilities:
self._script = script
self._clipboardHandlerId = None
self._lastIndentationData = {}
self._selectedMenuBarMenu = {}
#########################################################################
@@ -3126,27 +3127,193 @@ class Utilities:
return string
def indentationDescription(self, line):
if _settingsManager.getSetting('onlySpeakDisplayedText') \
or not _settingsManager.getSetting('enableSpeechIndentation'):
def _get_indentation_key(self, obj):
if obj is None:
return "global"
try:
return id(obj)
except Exception:
return str(obj)
@staticmethod
def _extract_indentation(line):
if not line:
return ""
line = line.replace("\u00a0", " ")
end = re.search("[^ \t]", line)
if end:
line = line[:end.start()]
return line[:end.start()]
result = ""
spaces = [m.span() for m in re.finditer(" +", line)]
tabs = [m.span() for m in re.finditer("\t+", line)]
return line
@staticmethod
def _get_indentation_segments(indentation):
if not indentation:
return []
spaces = [m.span() for m in re.finditer(" +", indentation)]
tabs = [m.span() for m in re.finditer("\t+", indentation)]
spans = sorted(spaces + tabs)
segments = []
for (start, end) in spans:
if (start, end) in spaces:
result += f"{messages.spacesCount(end - start)} "
segments.append(("spaces", end - start))
else:
result += f"{messages.tabsCount(end - start)} "
segments.append(("tabs", end - start))
return result
return segments
@staticmethod
def _get_indentation_columns(indentation, tab_width):
columns = 0
tab_width = max(1, tab_width)
for char in indentation:
if char == "\t":
columns += tab_width - (columns % tab_width)
else:
columns += 1
return columns
@staticmethod
def _get_indentation_levels(columns, spaces_per_level):
spaces_per_level = max(1, spaces_per_level)
if columns <= 0:
return 0
return int(math.ceil(columns / spaces_per_level))
def _get_indentation_data(self, line):
indentation = self._extract_indentation(line)
tab_width = _settingsManager.getSetting('indentationTabWidth') \
or settings.indentationTabWidth
spaces_per_level = _settingsManager.getSetting('indentationSpacesPerLevel') \
or settings.indentationSpacesPerLevel
columns = self._get_indentation_columns(indentation, tab_width)
levels = self._get_indentation_levels(columns, spaces_per_level)
segments = self._get_indentation_segments(indentation)
return {
"indentation": indentation,
"segments": segments,
"columns": columns,
"levels": levels,
}
def _remember_indentation(self, obj, data):
key = self._get_indentation_key(obj)
self._lastIndentationData[key] = {
"signature": data["indentation"],
"levels": data["levels"],
"columns": data["columns"],
}
def _indentation_has_changed(self, obj, data):
key = self._get_indentation_key(obj)
previous = self._lastIndentationData.get(key)
self._remember_indentation(obj, data)
if previous is None:
return True
change_mode = _settingsManager.getSetting('indentationChangeMode') \
or settings.indentationChangeMode
if change_mode == settings.INDENTATION_CHANGE_ANY:
return previous.get("signature") != data["indentation"]
if change_mode == settings.INDENTATION_CHANGE_LEVEL:
return previous.get("levels") != data["levels"]
return True
def _indentation_speech_enabled(self):
if _settingsManager.getSetting('onlySpeakDisplayedText'):
return False
if not _settingsManager.getSetting('enableSpeechIndentation'):
return False
presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
return presentation_mode in (
settings.INDENTATION_PRESENTATION_SPEECH,
settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
)
def _format_indentation_description(self, data):
style = _settingsManager.getSetting('indentationSpeechStyle') \
or settings.indentationSpeechStyle
if style == settings.INDENTATION_SPEECH_STYLE_LEVELS:
return messages.indentationLevelCount(data["levels"])
if style == settings.INDENTATION_SPEECH_STYLE_COLUMNS:
return messages.indentationColumnCount(data["columns"])
result = ""
for kind, count in data["segments"]:
if kind == "spaces":
result += f"{messages.spacesCount(count)} "
else:
result += f"{messages.tabsCount(count)} "
if not result:
return messages.spacesCount(0)
return result.strip()
def get_indentation_presentation(self, line, obj=None):
data = self._get_indentation_data(line)
has_indentation = bool(data["indentation"])
presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
only_if_changed = _settingsManager.getSetting('speakIndentationOnlyIfChanged')
change_mode = _settingsManager.getSetting('indentationChangeMode') \
or settings.indentationChangeMode
indent_debug = data["indentation"].replace("\t", "\\t").replace(" ", ".")
if not self._indentation_speech_enabled():
self._remember_indentation(obj, data)
msg = (
f"INDENTATION: speech disabled mode={presentation_mode} "
f"onlyIfChanged={only_if_changed} changeMode={change_mode} "
f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return "", has_indentation
if only_if_changed:
changed = self._indentation_has_changed(obj, data)
else:
changed = True
self._remember_indentation(obj, data)
if not changed:
msg = (
f"INDENTATION: unchanged mode={presentation_mode} "
f"onlyIfChanged={only_if_changed} changeMode={change_mode} "
f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return "", has_indentation
description = self._format_indentation_description(data)
msg = (
f"INDENTATION: speaking '{description}' mode={presentation_mode} "
f"onlyIfChanged={only_if_changed} changeMode={change_mode} "
f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return description, has_indentation
def should_strip_indentation(self, line):
presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
if presentation_mode == settings.INDENTATION_PRESENTATION_OFF:
return False
data = self._get_indentation_data(line)
return bool(data["indentation"])
def indentationDescription(self, line, obj=None):
description, _has_indentation = self.get_indentation_presentation(line, obj=obj)
return description
@staticmethod
def absoluteMouseCoordinates():
+10 -5
View File
@@ -2354,9 +2354,11 @@ class Script(script.Script):
[line, caretOffset, startOffset] = self.getTextLineAtCaret(obj)
if len(line) and line != "\n":
indentationDescription = self.utilities.indentationDescription(line)
indentationDescription, hasIndentation = \
self.utilities.get_indentation_presentation(line, obj=obj)
if indentationDescription:
self.speakMessage(indentationDescription)
stripIndentation = hasIndentation and self.utilities.should_strip_indentation(line)
endOffset = startOffset + len(line)
cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING)
@@ -2376,7 +2378,7 @@ class Script(script.Script):
# Some synthesizers will verbalize the whitespace, so if we've already
# described it, prevent double-presentation by stripping it off.
if not utterance and indentationDescription:
if not utterance and stripIndentation:
string = string.lstrip()
result = [string]
@@ -2406,13 +2408,16 @@ class Script(script.Script):
return
if len(phrase) > 1 or phrase.isalnum():
result = self.utilities.indentationDescription(phrase)
if result:
self.speakMessage(result)
indentationDescription, hasIndentation = \
self.utilities.get_indentation_presentation(phrase, obj=obj)
if indentationDescription:
self.speakMessage(indentationDescription)
cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING)
voice = self.speechGenerator.voice(obj=obj, string=phrase)
if hasIndentation and self.utilities.should_strip_indentation(phrase):
phrase = phrase.lstrip()
phrase = self.utilities.adjustForRepeats(phrase)
if self.utilities.shouldVerbalizeAllPunctuation(obj):
phrase = self.utilities.verbalizeAllPunctuation(phrase)
+42 -2
View File
@@ -48,6 +48,18 @@ userCustomizableSettings = [
"readFullRowInDocumentTable",
"readFullRowInSpreadSheet",
"enableSpeechIndentation",
"indentationPresentationMode",
"indentationChangeMode",
"speakIndentationOnlyIfChanged",
"indentationSpeechStyle",
"indentationAudioUnit",
"indentationSpacesPerLevel",
"indentationTabWidth",
"indentationAudioBaseFrequency",
"indentationAudioStepFrequency",
"indentationAudioMaxFrequency",
"indentationAudioDuration",
"indentationAudioVolume",
"enableEchoByCharacter",
"enableEchoByWord",
"enableEchoBySentence",
@@ -218,6 +230,22 @@ AI_SCREENSHOT_QUALITY_LOW = "low"
AI_SCREENSHOT_QUALITY_MEDIUM = "medium"
AI_SCREENSHOT_QUALITY_HIGH = "high"
INDENTATION_PRESENTATION_OFF = "off"
INDENTATION_PRESENTATION_SPEECH = "speech"
INDENTATION_PRESENTATION_BEEPS = "beeps"
INDENTATION_PRESENTATION_SPEECH_AND_BEEPS = "speech_and_beeps"
INDENTATION_CHANGE_ALWAYS = "always"
INDENTATION_CHANGE_ANY = "any"
INDENTATION_CHANGE_LEVEL = "level"
INDENTATION_SPEECH_STYLE_SPACES_TABS = "spaces_tabs"
INDENTATION_SPEECH_STYLE_LEVELS = "levels"
INDENTATION_SPEECH_STYLE_COLUMNS = "columns"
INDENTATION_UNIT_LEVELS = "levels"
INDENTATION_UNIT_COLUMNS = "columns"
DEFAULT_VOICE = "default"
UPPERCASE_VOICE = "uppercase"
HYPERLINK_VOICE = "hyperlink"
@@ -252,7 +280,19 @@ silenceSpeech = False
enableTutorialMessages = False
enableMnemonicSpeaking = False
enablePositionSpeaking = False
enableSpeechIndentation = False
enableSpeechIndentation = True
indentationPresentationMode = INDENTATION_PRESENTATION_SPEECH_AND_BEEPS
indentationChangeMode = INDENTATION_CHANGE_ANY
speakIndentationOnlyIfChanged = True
indentationSpeechStyle = INDENTATION_SPEECH_STYLE_COLUMNS
indentationAudioUnit = INDENTATION_UNIT_COLUMNS
indentationSpacesPerLevel = 4
indentationTabWidth = 4
indentationAudioBaseFrequency = 200
indentationAudioStepFrequency = 30
indentationAudioMaxFrequency = 2700
indentationAudioDuration = 0.15
indentationAudioVolume = 0.7
onlySpeakDisplayedText = False
presentToolTips = False
speakBlankLines = True
@@ -449,7 +489,7 @@ presentChatRoomLast = False
presentLiveRegionFromInactiveTab = False
# Plugins
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu']
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio']
# AI Assistant settings (disabled by default for opt-in behavior)
aiAssistantEnabled = True
@@ -967,11 +967,29 @@ class SpeechAndVerbosityManager:
"""Returns whether speaking of indentation and justification is enabled."""
return _settings_manager.getSetting('enableSpeechIndentation')
@staticmethod
def _sync_indentation_presentation_mode(enable_speech):
mode = _settings_manager.getSetting('indentationPresentationMode') \
or settings.indentationPresentationMode
if enable_speech:
if mode == settings.INDENTATION_PRESENTATION_OFF:
mode = settings.INDENTATION_PRESENTATION_SPEECH
elif mode == settings.INDENTATION_PRESENTATION_BEEPS:
mode = settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS
else:
if mode == settings.INDENTATION_PRESENTATION_SPEECH:
mode = settings.INDENTATION_PRESENTATION_OFF
elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
mode = settings.INDENTATION_PRESENTATION_BEEPS
_settings_manager.setSetting('indentationPresentationMode', mode)
@dbus_service.setter
def set_speak_indentation_and_justification(self, value: bool) -> bool:
"""Sets whether speaking of indentation and justification is enabled."""
try:
_settings_manager.setSetting('enableSpeechIndentation', value)
self._sync_indentation_presentation_mode(value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak indentation: {e}", True)
@@ -1804,6 +1822,7 @@ class SpeechAndVerbosityManager:
value = _settings_manager.getSetting('enableSpeechIndentation')
_settings_manager.setSetting('enableSpeechIndentation', not value)
self._sync_indentation_presentation_mode(not value)
if _settings_manager.getSetting('enableSpeechIndentation'):
full = messages.INDENTATION_JUSTIFICATION_ON_FULL
brief = messages.INDENTATION_JUSTIFICATION_ON_BRIEF
+18 -1
View File
@@ -193,6 +193,22 @@ class SpeechDBusManager:
return self._settings_manager.getSetting("enableSpeechIndentation")
def _sync_indentation_presentation_mode(self, enable_speech):
mode = self._settings_manager.getSetting("indentationPresentationMode") \
or settings.indentationPresentationMode
if enable_speech:
if mode == settings.INDENTATION_PRESENTATION_OFF:
mode = settings.INDENTATION_PRESENTATION_SPEECH
elif mode == settings.INDENTATION_PRESENTATION_BEEPS:
mode = settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS
else:
if mode == settings.INDENTATION_PRESENTATION_SPEECH:
mode = settings.INDENTATION_PRESENTATION_OFF
elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
mode = settings.INDENTATION_PRESENTATION_BEEPS
self._settings_manager.setSetting("indentationPresentationMode", mode)
@dbus_service.setter
def set_speak_indentation_and_justification(self, value: bool) -> bool:
"""Sets whether speaking of indentation and justification is enabled."""
@@ -200,6 +216,7 @@ class SpeechDBusManager:
msg = f"SPEECH DBUS MANAGER: Setting speak indentation and justification to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableSpeechIndentation", value)
self._sync_indentation_presentation_mode(value)
return True
@dbus_service.command
@@ -512,4 +529,4 @@ class SpeechDBusManager:
self._settings_manager.setSetting("enableEchoBySentence", new_sentence)
if script is not None:
script.presentMessage(full, brief)
script.presentMessage(full, brief)
+2 -1
View File
@@ -1685,7 +1685,8 @@ class SpeechGenerator(generator.Generator):
return []
line, caretOffset, startOffset = self._script.getTextLineAtCaret(obj)
description = self._script.utilities.indentationDescription(line)
description, _hasIndentation = \
self._script.utilities.get_indentation_presentation(line, obj=obj)
if not description:
return []