13
13
from PyQt5 .QtGui import (QTextCharFormat , QSyntaxHighlighter , QIcon , QFont , QKeySequence ,
14
14
QPainter , QPen , QColor , QFontMetrics , QTextDocument , QTextCursor )
15
15
from PyQt5 .QtWebEngineWidgets import QWebEngineView , QWebEnginePage
16
- from enchant import Dict , DictNotFoundError
17
16
from urllib .parse import quote
18
17
from snippet_editor_dialog import SnippetEditorDialog
19
- import os
20
18
from rss_reader import RSSReader
21
19
import json
22
20
import time
23
21
from theme_manager import ThemeManager
24
22
import hashlib
25
23
24
+ # Try enchant first, fallback to pyspellchecker
25
+ try :
26
+ from enchant import Dict , DictNotFoundError
27
+ USE_ENCHANT = True
28
+ except (ImportError , ModuleNotFoundError ) as e :
29
+ print ("Enchant not available, falling back to pyspellchecker:" , str (e ))
30
+ from spellchecker import SpellChecker
31
+ USE_ENCHANT = False
32
+
26
33
class SpellCheckHighlighter (QSyntaxHighlighter ):
27
34
def __init__ (self , parent , settings_manager ):
28
35
super ().__init__ (parent )
29
36
self .settings_manager = settings_manager
37
+ self .spell_check_enabled = True
38
+ self .USE_ENCHANT = USE_ENCHANT # Store the global flag
39
+
30
40
try :
31
- # Initialize enchant with English dictionary
32
- self .spell = Dict ("en_US" )
33
- self .spell_check_enabled = True
34
- print ("Spell checking enabled" )
35
-
36
- except DictNotFoundError as e :
37
- print (f"Warning: Spell checking disabled - { str (e )} " )
38
- self .spell_check_enabled = False
41
+ if self .USE_ENCHANT :
42
+ self .spell = Dict ("en_US" )
43
+ print ("Using Enchant for spell checking" )
44
+ else :
45
+ self .spell = SpellChecker ()
46
+ print ("Using pyspellchecker for spell checking" )
47
+ except Exception as e :
48
+ print (f"Spell checker initialization error: { str (e )} , falling back to pyspellchecker" )
49
+ self .spell = SpellChecker ()
50
+ self .USE_ENCHANT = False
39
51
40
- def is_latin_word (self , word ):
41
- """Check if word contains only Latin characters"""
42
- try :
43
- word .encode ('latin-1' )
52
+ def check_word (self , word ):
53
+ """Check if a word is spelled correctly"""
54
+ if not self .spell_check_enabled :
44
55
return True
45
- except UnicodeEncodeError :
46
- return False
56
+
57
+ if self .USE_ENCHANT :
58
+ return self .spell .check (word )
59
+ else :
60
+ # pyspellchecker considers unknown words misspelled
61
+ return word .lower () in self .spell
62
+
63
+ def suggest (self , word ):
64
+ """Get suggestions for a word"""
65
+ if not self .spell_check_enabled :
66
+ return []
67
+
68
+ # Get user dictionary
69
+ user_dict = self .settings_manager .get_setting ('user_dictionary' , [])
70
+
71
+ # Add matching words from user dictionary first
72
+ suggestions = [dict_word for dict_word in user_dict
73
+ if dict_word .lower ().startswith (word .lower ())]
74
+
75
+ # Only get spell checker suggestions for Latin words
76
+ if self .is_latin_word (word ):
77
+ try :
78
+ if self .USE_ENCHANT :
79
+ spell_suggestions = self .spell .suggest (word )
80
+ else :
81
+ spell_suggestions = self .spell .candidates (word )
82
+
83
+ if spell_suggestions :
84
+ # Remove the word itself from suggestions
85
+ spell_suggestions = [s for s in spell_suggestions
86
+ if s .lower () != word .lower ()]
87
+ suggestions .extend (spell_suggestions )
88
+ except UnicodeEncodeError :
89
+ pass
90
+
91
+ # Remove duplicates while preserving order
92
+ return list (dict .fromkeys (suggestions ))
93
+
94
+ def add_to_dictionary (self , word ):
95
+ """Add word to user dictionary"""
96
+ if self .USE_ENCHANT :
97
+ # Enchant spell checker implementation
98
+ self .spell .add (word )
99
+ else :
100
+ # PySpellChecker implementation
101
+ self .spell .word_frequency .add (word )
102
+ # Force a recheck of the document
103
+ self .highlighter .rehighlight ()
104
+
105
+ # Add to user dictionary in settings
106
+ user_dict = self .settings_manager .get_setting ('user_dictionary' , [])
107
+ if word not in user_dict :
108
+ user_dict .append (word )
109
+ self .settings_manager .save_setting ('user_dictionary' , user_dict )
47
110
48
111
def highlightBlock (self , text ):
49
112
if not self .spell_check_enabled :
@@ -67,52 +130,21 @@ def highlightBlock(self, text):
67
130
if self .is_latin_word (word ):
68
131
# Check if word is in user dictionary first
69
132
if word not in user_dict :
70
- # If not in user dictionary, check against enchant
71
133
try :
72
- if not self .spell . check (word ):
134
+ if not self .check_word (word ):
73
135
self .setFormat (index , length , format )
74
136
except UnicodeEncodeError :
75
137
pass # Skip words that can't be encoded
76
138
77
139
index = expression .indexIn (text , index + length )
78
140
79
- def suggest (self , word ):
80
- """Get suggestions for a word, including user dictionary matches"""
81
- suggestions = []
82
-
83
- # Get user dictionary
84
- user_dict = self .settings_manager .get_setting ('user_dictionary' , [])
85
-
86
- # Add matching words from user dictionary first
87
- for dict_word in user_dict :
88
- if dict_word .lower ().startswith (word .lower ()):
89
- suggestions .append (dict_word )
90
-
91
- # Only get enchant suggestions for Latin words
92
- if self .is_latin_word (word ):
93
- try :
94
- # Get suggestions from enchant
95
- spell_suggestions = self .spell .suggest (word )
96
- if spell_suggestions :
97
- # Remove the word itself from suggestions
98
- spell_suggestions = [s for s in spell_suggestions if s .lower () != word .lower ()]
99
- suggestions .extend (spell_suggestions )
100
- except UnicodeEncodeError :
101
- pass
102
-
103
- # Remove duplicates while preserving order
104
- return list (dict .fromkeys (suggestions ))
105
-
106
- def add_to_dictionary (self , word ):
107
- """Add word to user dictionary"""
108
- user_dict = self .settings_manager .get_setting ('user_dictionary' , [])
109
- if word not in user_dict :
110
- user_dict .append (word )
111
- self .settings_manager .save_setting ('user_dictionary' , user_dict )
112
- # Add to enchant personal word list
113
- self .spell .add (word )
114
- # Refresh spell checking
115
- self .rehighlight ()
141
+ def is_latin_word (self , word ):
142
+ """Check if word contains only Latin characters"""
143
+ try :
144
+ word .encode ('latin-1' )
145
+ return True
146
+ except UnicodeEncodeError :
147
+ return False
116
148
117
149
class CustomTextEdit (QTextEdit ):
118
150
def __init__ (self , parent = None ):
@@ -181,6 +213,16 @@ def __init__(self, parent=None):
181
213
self .completion_text = ""
182
214
self .completion_start = None
183
215
self .suppress_completion = False
216
+
217
+ # Initialize spell checker
218
+ if USE_ENCHANT :
219
+ try :
220
+ self .spell_checker = Dict ("en_US" )
221
+ except :
222
+ self .spell_checker = SpellChecker ()
223
+ print ("Fallback to pyspellchecker in CompletingTextEdit" )
224
+ else :
225
+ self .spell_checker = SpellChecker ()
184
226
185
227
def keyPressEvent (self , event ):
186
228
"""Handle key events"""
@@ -292,13 +334,19 @@ def __init__(self, snippet_manager, settings_manager):
292
334
self .web_view = None # Initialize to None
293
335
self .main_window = None # Initialize main_window to None
294
336
295
- # # Initialize recovery ID and paths first
296
- # self.recovery_id = str(int(time.time() * 1000))
297
- # self.session_path = os.path.join(
298
- # self.settings_manager.get_recovery_dir(),
299
- # f"session_{self.recovery_id}.txt"
300
- # )
301
- # self.meta_path = self.session_path + '.json'
337
+ # Add USE_ENCHANT as instance attribute
338
+ self .USE_ENCHANT = USE_ENCHANT # Use the module-level variable
339
+
340
+ # Initialize spell checker
341
+ if self .USE_ENCHANT :
342
+ try :
343
+ self .spell_checker = Dict ("en_US" )
344
+ except :
345
+ self .USE_ENCHANT = False # Fall back if enchant fails
346
+ self .spell_checker = SpellChecker ()
347
+ print ("Fallback to pyspellchecker in EditorTab" )
348
+ else :
349
+ self .spell_checker = SpellChecker ()
302
350
303
351
# Setup UI components
304
352
self .setup_ui ()
@@ -688,21 +736,24 @@ def show_context_menu(self, pos):
688
736
cursor .select (cursor .WordUnderCursor )
689
737
self .editor .setTextCursor (cursor )
690
738
739
+ # Create menu with a slight delay to prevent accidental triggers
740
+ QTimer .singleShot (100 , lambda : self ._show_context_menu_impl (pos ))
741
+
742
+ def _show_context_menu_impl (self , pos ):
743
+ """Implementation of context menu display"""
691
744
menu = QMenu (self )
692
745
693
- # Cut/Copy/Paste actions
694
- menu .addAction ("Cut" , self .editor .cut )
695
- menu .addAction ("Copy" , self .editor .copy )
696
- menu .addAction ("Paste" , self .editor .paste )
697
- menu .addSeparator ()
746
+ # # Cut/Copy/Paste actions
747
+ # menu.addAction("Cut", self.editor.cut)
748
+ # menu.addAction("Copy", self.editor.copy)
749
+ # menu.addAction("Paste", self.editor.paste)
750
+ # menu.addSeparator()
698
751
699
752
# Get selected text
700
- selected_text = cursor .selectedText ()
753
+ selected_text = self . editor . textCursor () .selectedText ()
701
754
702
755
if selected_text :
703
- # Add "Save as Snippet" option
704
- menu .addAction ("Save as Snippet" , lambda : self .save_snippet (selected_text ))
705
- menu .addSeparator ()
756
+
706
757
707
758
# Add search submenu
708
759
search_menu = menu .addMenu ("Search in..." )
@@ -783,6 +834,14 @@ def show_context_menu(self, pos):
783
834
add_action = menu .addAction ("Add to Dictionary" )
784
835
add_action .triggered .connect (lambda : self .add_to_dictionary (selected_text ))
785
836
menu .addSeparator ()
837
+ # Add "Save as Snippet" option
838
+ menu .addAction ("Save as Snippet" , lambda : self .save_snippet (selected_text ))
839
+ menu .addSeparator ()
840
+ # Cut/Copy/Paste actions
841
+ menu .addAction ("Cut" , self .editor .cut )
842
+ menu .addAction ("Copy" , self .editor .copy )
843
+ menu .addAction ("Paste" , self .editor .paste )
844
+ menu .addSeparator ()
786
845
787
846
# Show menu
788
847
menu .exec_ (self .editor .mapToGlobal (pos ))
@@ -819,14 +878,20 @@ def search_in_browser(self, url):
819
878
820
879
def add_to_dictionary (self , word ):
821
880
"""Add word to user dictionary"""
881
+ if self .USE_ENCHANT :
882
+ # Enchant spell checker implementation
883
+ self .spell_checker .add (word )
884
+ else :
885
+ # PySpellChecker implementation
886
+ self .spell_checker .word_frequency .add (word )
887
+ # Force a recheck of the document
888
+ self .highlighter .rehighlight ()
889
+
890
+ # Add to user dictionary in settings
822
891
user_dict = self .settings_manager .get_setting ('user_dictionary' , [])
823
892
if word not in user_dict :
824
893
user_dict .append (word )
825
894
self .settings_manager .save_setting ('user_dictionary' , user_dict )
826
- # Add to enchant personal word list
827
- self .highlighter .spell .add (word )
828
- # Refresh spell checking
829
- self .highlighter .rehighlight ()
830
895
831
896
def ensure_browser_visible (self ):
832
897
"""Ensure browser pane is visible"""
@@ -1565,9 +1630,9 @@ def set_main_window(self, main_window):
1565
1630
self .main_window = main_window
1566
1631
# # Update session state to include this tab
1567
1632
# current_tabs = self.main_window.get_open_tab_ids()
1568
- # if self.recovery_id not in current_tabs:
1569
- # current_tabs.append(self.recovery_id)
1570
- # self.settings_manager.save_session_state(current_tabs)
1633
+ # # if self.recovery_id not in current_tabs:
1634
+ # # current_tabs.append(self.recovery_id)
1635
+ # # self.settings_manager.save_session_state(current_tabs)
1571
1636
1572
1637
# def cleanup_session_files(self):
1573
1638
# """Clean up session files for this tab"""
0 commit comments