Merge pull request #118 from MightyCreak/pylint
Extract Preferences and Resources in their own modules
This commit is contained in:
commit
74ef6650a0
17
.pylintrc
17
.pylintrc
|
@ -87,23 +87,24 @@ disable=raw-checker-failed,
|
|||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
|
||||
# temporary silenced messages
|
||||
# temporary silenced messages (ordered alphabetically)
|
||||
duplicate-code,
|
||||
fixme,
|
||||
import-error,
|
||||
invalid-name,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
import-error,
|
||||
missing-module-docstring,
|
||||
no-self-use,
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-locals,
|
||||
too-many-statements,
|
||||
too-many-nested-blocks,
|
||||
too-few-public-methods,
|
||||
unused-argument,
|
||||
fixme,
|
||||
too-many-statements,
|
||||
unnecessary-dict-index-lookup,
|
||||
duplicate-code
|
||||
unused-argument
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
|
|
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Added a flatpak job in the CI
|
||||
|
||||
### Changed
|
||||
- main.py slimmed down by about 1000 lines
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
967
src/main.py
967
src/main.py
File diff suppressed because it is too large
Load Diff
|
@ -33,6 +33,8 @@ configure_file(
|
|||
diffuse_sources = [
|
||||
'__init__.py',
|
||||
'main.py',
|
||||
'preferences.py',
|
||||
'resources.py',
|
||||
'utils.py',
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,479 @@
|
|||
# Diffuse: a graphical tool for merging and comparing text files.
|
||||
#
|
||||
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
|
||||
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import codecs
|
||||
import encodings
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
from diffuse import utils
|
||||
from diffuse import constants
|
||||
|
||||
# class to store preferences and construct a dialogue for manipulating them
|
||||
class Preferences:
|
||||
def __init__(self, path):
|
||||
self.bool_prefs = {}
|
||||
self.int_prefs = {}
|
||||
self.string_prefs = {}
|
||||
self.int_prefs_min = {}
|
||||
self.int_prefs_max = {}
|
||||
self.string_prefs_enums = {}
|
||||
|
||||
# find available encodings
|
||||
self.encodings = sorted(set(encodings.aliases.aliases.values()))
|
||||
|
||||
if utils.isWindows():
|
||||
svk_bin = 'svk.bat'
|
||||
else:
|
||||
svk_bin = 'svk'
|
||||
|
||||
auto_detect_codecs = [ 'utf_8', 'utf_16', 'latin_1' ]
|
||||
e = utils.norm_encoding(sys.getfilesystemencoding())
|
||||
if e not in auto_detect_codecs:
|
||||
# insert after UTF-8 as the default encoding may prevent UTF-8 from
|
||||
# being tried
|
||||
auto_detect_codecs.insert(2, e)
|
||||
|
||||
# self.template describes how preference dialogue layout
|
||||
#
|
||||
# this will be traversed later to build the preferences dialogue and
|
||||
# discover which preferences exist
|
||||
#
|
||||
# folders are described using:
|
||||
# [ 'FolderSet', label1, template1, label2, template2, ... ]
|
||||
# lists are described using:
|
||||
# [ 'List', template1, template2, template3, ... ]
|
||||
# individual preferences are described using one of the following
|
||||
# depending upon its type and the desired widget:
|
||||
# [ 'Boolean', name, default, label ]
|
||||
# [ 'Integer', name, default, label ]
|
||||
# [ 'String', name, default, label ]
|
||||
# [ 'File', name, default, label ]
|
||||
# [ 'Font', name, default, label ]
|
||||
# pylint: disable=line-too-long
|
||||
self.template = [
|
||||
'FolderSet',
|
||||
_('Display'),
|
||||
[ 'List',
|
||||
[ 'Font', 'display_font', 'Monospace 10', _('Font') ],
|
||||
[ 'Integer', 'display_tab_width', 8, _('Tab width'), 1, 1024 ],
|
||||
[ 'Boolean', 'display_show_right_margin', True, _('Show right margin') ],
|
||||
[ 'Integer', 'display_right_margin', 80, _('Right margin'), 1, 8192 ],
|
||||
[ 'Boolean', 'display_show_line_numbers', True, _('Show line numbers') ],
|
||||
[ 'Boolean', 'display_show_whitespace', False, _('Show white space characters') ],
|
||||
[ 'Boolean', 'display_ignore_case', False, _('Ignore case differences') ],
|
||||
[ 'Boolean', 'display_ignore_whitespace', False, _('Ignore white space differences') ],
|
||||
[ 'Boolean', 'display_ignore_whitespace_changes', False, _('Ignore changes to white space') ],
|
||||
[ 'Boolean', 'display_ignore_blanklines', False, _('Ignore blank line differences') ],
|
||||
[ 'Boolean', 'display_ignore_endofline', False, _('Ignore end of line differences') ]
|
||||
],
|
||||
_('Alignment'),
|
||||
[ 'List',
|
||||
[ 'Boolean', 'align_ignore_case', False, _('Ignore case') ],
|
||||
[ 'Boolean', 'align_ignore_whitespace', True, _('Ignore white space') ],
|
||||
[ 'Boolean', 'align_ignore_whitespace_changes', False, _('Ignore changes to white space') ],
|
||||
[ 'Boolean', 'align_ignore_blanklines', False, _('Ignore blank lines') ],
|
||||
[ 'Boolean', 'align_ignore_endofline', True, _('Ignore end of line characters') ]
|
||||
],
|
||||
_('Editor'),
|
||||
[ 'List',
|
||||
[ 'Boolean', 'editor_auto_indent', True, _('Auto indent') ],
|
||||
[ 'Boolean', 'editor_expand_tabs', False, _('Expand tabs to spaces') ],
|
||||
[ 'Integer', 'editor_soft_tab_width', 8, _('Soft tab width'), 1, 1024 ]
|
||||
],
|
||||
_('Tabs'),
|
||||
[ 'List',
|
||||
[ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ],
|
||||
[ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ],
|
||||
[ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % constants.APP_NAME ]
|
||||
],
|
||||
_('Regional Settings'),
|
||||
[ 'List',
|
||||
[ 'Encoding', 'encoding_default_codec', sys.getfilesystemencoding(), _('Default codec') ],
|
||||
[ 'String', 'encoding_auto_detect_codecs', ' '.join(auto_detect_codecs), _('Order of codecs used to identify encoding') ]
|
||||
],
|
||||
]
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# conditions used to determine if a preference should be greyed out
|
||||
self.disable_when = {
|
||||
'display_right_margin': ('display_show_right_margin', False),
|
||||
'display_ignore_whitespace_changes': ('display_ignore_whitespace', True),
|
||||
'display_ignore_blanklines': ('display_ignore_whitespace', True),
|
||||
'display_ignore_endofline': ('display_ignore_whitespace', True),
|
||||
'align_ignore_whitespace_changes': ('align_ignore_whitespace', True),
|
||||
'align_ignore_blanklines': ('align_ignore_whitespace', True),
|
||||
'align_ignore_endofline': ('align_ignore_whitespace', True)
|
||||
}
|
||||
if utils.isWindows():
|
||||
root = os.environ.get('SYSTEMDRIVE', None)
|
||||
if root is None:
|
||||
root = 'C:\\'
|
||||
elif not root.endswith('\\'):
|
||||
root += '\\'
|
||||
self.template.extend([
|
||||
_('Cygwin'),
|
||||
[ 'List',
|
||||
[ 'File', 'cygwin_root', os.path.join(root, 'cygwin'), _('Root directory') ],
|
||||
[ 'String', 'cygwin_cygdrive_prefix', '/cygdrive', _('Cygdrive prefix') ]
|
||||
]
|
||||
])
|
||||
|
||||
# create template for Version Control options
|
||||
vcs = [ ('bzr', 'Bazaar', 'bzr'),
|
||||
('cvs', 'CVS', 'cvs'),
|
||||
('darcs', 'Darcs', 'darcs'),
|
||||
('git', 'Git', 'git'),
|
||||
('hg', 'Mercurial', 'hg'),
|
||||
('mtn', 'Monotone', 'mtn'),
|
||||
('rcs', 'RCS', None),
|
||||
('svn', 'Subversion', 'svn'),
|
||||
('svk', 'SVK', svk_bin) ]
|
||||
|
||||
vcs_template = [
|
||||
'List', [
|
||||
'String',
|
||||
'vcs_search_order',
|
||||
'bzr cvs darcs git hg mtn rcs svn svk',
|
||||
_('Version control system search order')
|
||||
]
|
||||
]
|
||||
vcs_folders_template = [ 'FolderSet' ]
|
||||
for key, name, cmd in vcs:
|
||||
temp = [ 'List' ]
|
||||
if key == 'rcs':
|
||||
# RCS uses multiple commands
|
||||
temp.extend([ [ 'File', key + '_bin_co', 'co', _('"co" command') ],
|
||||
[ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ])
|
||||
else:
|
||||
temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ])
|
||||
if utils.isWindows():
|
||||
temp.append([
|
||||
'Boolean',
|
||||
key + '_bash',
|
||||
False,
|
||||
_('Launch from a Bash login shell')
|
||||
])
|
||||
if key != 'git':
|
||||
temp.append([
|
||||
'Boolean',
|
||||
key + '_cygwin',
|
||||
False,
|
||||
_('Update paths for Cygwin')
|
||||
])
|
||||
vcs_folders_template.extend([ name, temp ])
|
||||
vcs_template.append(vcs_folders_template)
|
||||
|
||||
self.template.extend([ _('Version Control'), vcs_template ])
|
||||
self._initFromTemplate(self.template)
|
||||
self.default_bool_prefs = self.bool_prefs.copy()
|
||||
self.default_int_prefs = self.int_prefs.copy()
|
||||
self.default_string_prefs = self.string_prefs.copy()
|
||||
# load the user's preferences
|
||||
self.path = path
|
||||
if os.path.isfile(self.path):
|
||||
try:
|
||||
with open(self.path, 'r', encoding='utf-8') as f:
|
||||
ss = utils.readconfiglines(f)
|
||||
for j, s in enumerate(ss):
|
||||
try:
|
||||
a = shlex.split(s, True)
|
||||
if len(a) > 0:
|
||||
p = a[0]
|
||||
if len(a) == 2 and p in self.bool_prefs:
|
||||
self.bool_prefs[p] = (a[1] == 'True')
|
||||
elif len(a) == 2 and p in self.int_prefs:
|
||||
self.int_prefs[p] = max(self.int_prefs_min[p], min(int(a[1]), self.int_prefs_max[p]))
|
||||
elif len(a) == 2 and p in self.string_prefs:
|
||||
self.string_prefs[p] = a[1]
|
||||
else:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
# this may happen if the prefs were written by a
|
||||
# different version -- don't bother the user
|
||||
utils.logDebug(f'Error processing line {j + 1} of {self.path}.')
|
||||
except IOError:
|
||||
# bad $HOME value? -- don't bother the user
|
||||
utils.logDebug(f'Error reading {self.path}.')
|
||||
|
||||
# recursively traverses 'template' to discover the preferences and
|
||||
# initialise their default values in self.bool_prefs, self.int_prefs, and
|
||||
# self.string_prefs
|
||||
def _initFromTemplate(self, template):
|
||||
if template[0] == 'FolderSet' or template[0] == 'List':
|
||||
i = 1
|
||||
while i < len(template):
|
||||
if template[0] == 'FolderSet':
|
||||
i += 1
|
||||
self._initFromTemplate(template[i])
|
||||
i += 1
|
||||
elif template[0] == 'Boolean':
|
||||
self.bool_prefs[template[1]] = template[2]
|
||||
elif template[0] == 'Integer':
|
||||
self.int_prefs[template[1]] = template[2]
|
||||
self.int_prefs_min[template[1]] = template[4]
|
||||
self.int_prefs_max[template[1]] = template[5]
|
||||
elif template[0] in [ 'String', 'File', 'Font', 'Encoding' ]:
|
||||
self.string_prefs[template[1]] = template[2]
|
||||
|
||||
# callback used when a preference is toggled
|
||||
def _toggled_cb(self, widget, widgets, name):
|
||||
# disable any preferences than are no longer relevant
|
||||
for k, v in self.disable_when.items():
|
||||
p, t = v
|
||||
if p == name:
|
||||
widgets[k].set_sensitive(widgets[p].get_active() != t)
|
||||
|
||||
# display the dialogue and update the preference values if the accept
|
||||
# button was pressed
|
||||
def runDialog(self, parent):
|
||||
dialog = Gtk.Dialog(_('Preferences'), parent=parent, destroy_with_parent=True)
|
||||
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)
|
||||
dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
|
||||
widgets = {}
|
||||
w = self._buildPrefsDialog(parent, widgets, self.template)
|
||||
# disable any preferences than are not relevant
|
||||
for k, v in self.disable_when.items():
|
||||
p, t = v
|
||||
if widgets[p].get_active() == t:
|
||||
widgets[k].set_sensitive(False)
|
||||
dialog.vbox.add(w) # pylint: disable=no-member
|
||||
w.show()
|
||||
|
||||
accept = (dialog.run() == Gtk.ResponseType.OK) # pylint: disable=no-member
|
||||
if accept:
|
||||
for k in self.bool_prefs:
|
||||
self.bool_prefs[k] = widgets[k].get_active()
|
||||
for k in self.int_prefs:
|
||||
self.int_prefs[k] = widgets[k].get_value_as_int()
|
||||
for k in self.string_prefs:
|
||||
self.string_prefs[k] = utils.null_to_empty(widgets[k].get_text())
|
||||
try:
|
||||
ss = []
|
||||
for k, v in self.bool_prefs.items():
|
||||
if v != self.default_bool_prefs[k]:
|
||||
ss.append(f'{k} {v}\n')
|
||||
for k, v in self.int_prefs.items():
|
||||
if v != self.default_int_prefs[k]:
|
||||
ss.append(f'{k} {v}\n')
|
||||
for k, v in self.string_prefs.items():
|
||||
if v != self.default_string_prefs[k]:
|
||||
v_escaped = v.replace('\\', '\\\\').replace('"', '\\"')
|
||||
ss.append(f'{k} "{v_escaped}"\n')
|
||||
ss.sort()
|
||||
with open(self.path, 'w', encoding='utf-8') as f:
|
||||
# pylint: disable-next=line-too-long
|
||||
f.write(f'# This prefs file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n')
|
||||
for s in ss:
|
||||
f.write(s)
|
||||
except IOError:
|
||||
utils.logErrorAndDialog(_('Error writing %s.') % (self.path, ), parent)
|
||||
dialog.destroy()
|
||||
return accept
|
||||
|
||||
# recursively traverses 'template' to build the preferences dialogue
|
||||
# and the individual preference widgets into 'widgets' so their value
|
||||
# can be easily queried by the caller
|
||||
def _buildPrefsDialog(self, parent, widgets, template):
|
||||
tpl_section = template[0]
|
||||
if tpl_section == 'FolderSet':
|
||||
notebook = Gtk.Notebook.new()
|
||||
notebook.set_border_width(10)
|
||||
i = 1
|
||||
while i < len(template):
|
||||
label = Gtk.Label.new(template[i])
|
||||
i += 1
|
||||
w = self._buildPrefsDialog(parent, widgets, template[i])
|
||||
i += 1
|
||||
notebook.append_page(w, label)
|
||||
w.show()
|
||||
label.show()
|
||||
return notebook
|
||||
|
||||
table = Gtk.Grid.new()
|
||||
table.set_border_width(10)
|
||||
for i, tpl in enumerate(template[1:]):
|
||||
tpl_section = tpl[0]
|
||||
if tpl_section == 'FolderSet':
|
||||
w = self._buildPrefsDialog(parent, widgets, tpl)
|
||||
table.attach(w, 0, i, 2, 1)
|
||||
w.show()
|
||||
elif tpl_section == 'Boolean':
|
||||
button = Gtk.CheckButton.new_with_mnemonic(tpl[3])
|
||||
button.set_active(self.bool_prefs[tpl[1]])
|
||||
widgets[tpl[1]] = button
|
||||
table.attach(button, 1, i, 1, 1)
|
||||
button.connect('toggled', self._toggled_cb, widgets, tpl[1])
|
||||
button.show()
|
||||
else:
|
||||
label = Gtk.Label.new(tpl[3] + ': ')
|
||||
label.set_xalign(1.0)
|
||||
label.set_yalign(0.5)
|
||||
table.attach(label, 0, i, 1, 1)
|
||||
label.show()
|
||||
if tpl[0] in [ 'Font', 'Integer' ]:
|
||||
entry = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
|
||||
if tpl[0] == 'Font':
|
||||
button = _FontButton()
|
||||
button.set_font(self.string_prefs[tpl[1]])
|
||||
else:
|
||||
button = Gtk.SpinButton.new(
|
||||
Gtk.Adjustment.new(self.int_prefs[tpl[1]], tpl[4], tpl[5], 1, 0, 0),
|
||||
1.0,
|
||||
0)
|
||||
widgets[tpl[1]] = button
|
||||
entry.pack_start(button, False, False, 0)
|
||||
button.show()
|
||||
else:
|
||||
if tpl[0] == 'Encoding':
|
||||
entry = utils.EncodingMenu(self)
|
||||
entry.set_text(tpl[3])
|
||||
elif tpl[0] == 'File':
|
||||
entry = _FileEntry(parent, tpl[3])
|
||||
else:
|
||||
entry = Gtk.Entry.new()
|
||||
widgets[tpl[1]] = entry
|
||||
entry.set_text(self.string_prefs[tpl[1]])
|
||||
table.attach(entry, 1, i, 1, 1)
|
||||
entry.show()
|
||||
table.show()
|
||||
return table
|
||||
|
||||
# get/set methods to manipulate the preference values
|
||||
def getBool(self, name):
|
||||
return self.bool_prefs[name]
|
||||
|
||||
def setBool(self, name, value):
|
||||
self.bool_prefs[name] = value
|
||||
|
||||
def getInt(self, name):
|
||||
return self.int_prefs[name]
|
||||
|
||||
def getString(self, name):
|
||||
return self.string_prefs[name]
|
||||
|
||||
def setString(self, name, value):
|
||||
self.string_prefs[name] = value
|
||||
|
||||
def getEncodings(self):
|
||||
return self.encodings
|
||||
|
||||
def _getDefaultEncodings(self):
|
||||
return self.string_prefs['encoding_auto_detect_codecs'].split()
|
||||
|
||||
def getDefaultEncoding(self):
|
||||
return self.string_prefs['encoding_default_codec']
|
||||
|
||||
# attempt to convert a string to unicode from an unknown encoding
|
||||
def convertToUnicode(self, s):
|
||||
# a BOM is required for autodetecting UTF16 and UTF32
|
||||
magic = { 'utf16': [ codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE ],
|
||||
'utf32': [ codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE ] }
|
||||
for encoding in self._getDefaultEncodings():
|
||||
try:
|
||||
encoding = encoding.lower().replace('-', '').replace('_', '')
|
||||
for m in magic.get(encoding, [ b'' ]):
|
||||
if s.startswith(m):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
return str(s, encoding=encoding), encoding
|
||||
except (UnicodeDecodeError, LookupError):
|
||||
pass
|
||||
return ''.join([ chr(ord(c)) for c in s ]), None
|
||||
|
||||
# cygwin and native applications can be used on windows, use this method
|
||||
# to convert a path to the usual form expected on sys.platform
|
||||
def convertToNativePath(self, s):
|
||||
if utils.isWindows() and s.find('/') >= 0:
|
||||
# treat as a cygwin path
|
||||
s = s.replace(os.sep, '/')
|
||||
# convert to a Windows native style path
|
||||
p = [ a for a in s.split('/') if a != '' ]
|
||||
if s.startswith('//'):
|
||||
p[:0] = [ '', '' ]
|
||||
elif s.startswith('/'):
|
||||
pr = [ a for a in self.getString('cygwin_cygdrive_prefix').split('/') if a != '' ]
|
||||
n = len(pr)
|
||||
if len(p) > n and len(p[n]) == 1 and p[:n] == pr:
|
||||
# path starts with cygdrive prefix
|
||||
p[:n + 1] = [ p[n] + ':' ]
|
||||
else:
|
||||
# full path
|
||||
p[:0] = [ a for a in self.getString('cygwin_root').split(os.sep) if a != '' ]
|
||||
# add trailing slash
|
||||
if p[-1] != '' and s.endswith('/'):
|
||||
p.append('')
|
||||
s = os.sep.join(p)
|
||||
return s
|
||||
|
||||
# adaptor class to allow a Gtk.FontButton to be read like a Gtk.Entry
|
||||
class _FontButton(Gtk.FontButton):
|
||||
def __init__(self):
|
||||
Gtk.FontButton.__init__(self)
|
||||
|
||||
def get_text(self):
|
||||
# pylint: disable=no-member
|
||||
return self.get_font_name()
|
||||
|
||||
# text entry widget with a button to help pick file names
|
||||
class _FileEntry(Gtk.Box):
|
||||
def __init__(self, parent, title):
|
||||
# pylint: disable=no-member
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.toplevel = parent
|
||||
self.title = title
|
||||
self.entry = entry = Gtk.Entry.new()
|
||||
self.pack_start(entry, True, True, 0)
|
||||
entry.show()
|
||||
button = Gtk.Button.new()
|
||||
image = Gtk.Image.new()
|
||||
image.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU)
|
||||
button.add(image)
|
||||
image.show()
|
||||
button.connect('clicked', self.chooseFile)
|
||||
self.pack_start(button, False, False, 0)
|
||||
button.show()
|
||||
|
||||
# action performed when the pick file button is pressed
|
||||
def chooseFile(self, widget):
|
||||
# pylint: disable=no-member
|
||||
dialog = Gtk.FileChooserDialog(
|
||||
self.title,
|
||||
self.toplevel,
|
||||
Gtk.FileChooserAction.OPEN,
|
||||
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
|
||||
dialog.set_current_folder(os.path.realpath(os.curdir))
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
self.entry.set_text(dialog.get_filename())
|
||||
dialog.destroy()
|
||||
|
||||
def set_text(self, s):
|
||||
self.entry.set_text(s)
|
||||
|
||||
def get_text(self):
|
||||
return self.entry.get_text()
|
|
@ -0,0 +1,537 @@
|
|||
# Diffuse: a graphical tool for merging and comparing text files.
|
||||
#
|
||||
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
|
||||
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# This class to hold all customisable behaviour not exposed in the preferences
|
||||
# dialogue: hotkey assignment, colours, syntax highlighting, etc.
|
||||
# Syntax highlighting is implemented in supporting '*.syntax' files normally
|
||||
# read from the system wide initialisation file '/etc/diffuserc'.
|
||||
# The personal initialisation file '~/diffuse/diffuserc' can be used to change
|
||||
# default behaviour.
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
import gi
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
from diffuse import utils
|
||||
|
||||
class Resources:
|
||||
def __init__(self):
|
||||
# default keybindings
|
||||
self.keybindings = {}
|
||||
self.keybindings_lookup = {}
|
||||
set_binding = self.setKeyBinding
|
||||
set_binding('menu', 'open_file', 'Ctrl+o')
|
||||
set_binding('menu', 'open_file_in_new_tab', 'Ctrl+t')
|
||||
set_binding('menu', 'open_modified_files', 'Shift+Ctrl+O')
|
||||
set_binding('menu', 'open_commit', 'Shift+Ctrl+T')
|
||||
set_binding('menu', 'reload_file', 'Shift+Ctrl+R')
|
||||
set_binding('menu', 'save_file', 'Ctrl+s')
|
||||
set_binding('menu', 'save_file_as', 'Shift+Ctrl+A')
|
||||
set_binding('menu', 'save_all', 'Shift+Ctrl+S')
|
||||
set_binding('menu', 'new_2_way_file_merge', 'Ctrl+2')
|
||||
set_binding('menu', 'new_3_way_file_merge', 'Ctrl+3')
|
||||
set_binding('menu', 'new_n_way_file_merge', 'Ctrl+4')
|
||||
set_binding('menu', 'close_tab', 'Ctrl+w')
|
||||
set_binding('menu', 'undo_close_tab', 'Shift+Ctrl+W')
|
||||
set_binding('menu', 'quit', 'Ctrl+q')
|
||||
set_binding('menu', 'undo', 'Ctrl+z')
|
||||
set_binding('menu', 'redo', 'Shift+Ctrl+Z')
|
||||
set_binding('menu', 'cut', 'Ctrl+x')
|
||||
set_binding('menu', 'copy', 'Ctrl+c')
|
||||
set_binding('menu', 'paste', 'Ctrl+v')
|
||||
set_binding('menu', 'select_all', 'Ctrl+a')
|
||||
set_binding('menu', 'clear_edits', 'Ctrl+r')
|
||||
set_binding('menu', 'dismiss_all_edits', 'Ctrl+d')
|
||||
set_binding('menu', 'find', 'Ctrl+f')
|
||||
set_binding('menu', 'find_next', 'Ctrl+g')
|
||||
set_binding('menu', 'find_previous', 'Shift+Ctrl+G')
|
||||
set_binding('menu', 'go_to_line', 'Shift+Ctrl+L')
|
||||
set_binding('menu', 'realign_all', 'Ctrl+l')
|
||||
set_binding('menu', 'isolate', 'Ctrl+i')
|
||||
set_binding('menu', 'first_difference', 'Shift+Ctrl+Up')
|
||||
set_binding('menu', 'previous_difference', 'Ctrl+Up')
|
||||
set_binding('menu', 'next_difference', 'Ctrl+Down')
|
||||
set_binding('menu', 'last_difference', 'Shift+Ctrl+Down')
|
||||
set_binding('menu', 'first_tab', 'Shift+Ctrl+Page_Up')
|
||||
set_binding('menu', 'previous_tab', 'Ctrl+Page_Up')
|
||||
set_binding('menu', 'next_tab', 'Ctrl+Page_Down')
|
||||
set_binding('menu', 'last_tab', 'Shift+Ctrl+Page_Down')
|
||||
set_binding('menu', 'shift_pane_right', 'Shift+Ctrl+parenright')
|
||||
set_binding('menu', 'shift_pane_left', 'Shift+Ctrl+parenleft')
|
||||
set_binding('menu', 'convert_to_upper_case', 'Ctrl+u')
|
||||
set_binding('menu', 'convert_to_lower_case', 'Shift+Ctrl+U')
|
||||
set_binding('menu', 'sort_lines_in_ascending_order', 'Ctrl+y')
|
||||
set_binding('menu', 'sort_lines_in_descending_order', 'Shift+Ctrl+Y')
|
||||
set_binding('menu', 'remove_trailing_white_space', 'Ctrl+k')
|
||||
set_binding('menu', 'convert_tabs_to_spaces', 'Ctrl+b')
|
||||
set_binding('menu', 'convert_leading_spaces_to_tabs', 'Shift+Ctrl+B')
|
||||
set_binding('menu', 'increase_indenting', 'Shift+Ctrl+greater')
|
||||
set_binding('menu', 'decrease_indenting', 'Shift+Ctrl+less')
|
||||
set_binding('menu', 'convert_to_dos', 'Shift+Ctrl+E')
|
||||
set_binding('menu', 'convert_to_mac', 'Shift+Ctrl+C')
|
||||
set_binding('menu', 'convert_to_unix', 'Ctrl+e')
|
||||
set_binding('menu', 'copy_selection_right', 'Shift+Ctrl+Right')
|
||||
set_binding('menu', 'copy_selection_left', 'Shift+Ctrl+Left')
|
||||
set_binding('menu', 'copy_left_into_selection', 'Ctrl+Right')
|
||||
set_binding('menu', 'copy_right_into_selection', 'Ctrl+Left')
|
||||
set_binding('menu', 'merge_from_left_then_right', 'Ctrl+m')
|
||||
set_binding('menu', 'merge_from_right_then_left', 'Shift+Ctrl+M')
|
||||
set_binding('menu', 'help_contents', 'F1')
|
||||
set_binding('line_mode', 'enter_align_mode', 'space')
|
||||
set_binding('line_mode', 'enter_character_mode', 'Return')
|
||||
set_binding('line_mode', 'enter_character_mode', 'KP_Enter')
|
||||
set_binding('line_mode', 'first_line', 'Home')
|
||||
set_binding('line_mode', 'first_line', 'g')
|
||||
set_binding('line_mode', 'extend_first_line', 'Shift+Home')
|
||||
set_binding('line_mode', 'last_line', 'End')
|
||||
set_binding('line_mode', 'last_line', 'Shift+G')
|
||||
set_binding('line_mode', 'extend_last_line', 'Shift+End')
|
||||
set_binding('line_mode', 'up', 'Up')
|
||||
set_binding('line_mode', 'up', 'k')
|
||||
set_binding('line_mode', 'extend_up', 'Shift+Up')
|
||||
set_binding('line_mode', 'extend_up', 'Shift+K')
|
||||
set_binding('line_mode', 'down', 'Down')
|
||||
set_binding('line_mode', 'down', 'j')
|
||||
set_binding('line_mode', 'extend_down', 'Shift+Down')
|
||||
set_binding('line_mode', 'extend_down', 'Shift+J')
|
||||
set_binding('line_mode', 'left', 'Left')
|
||||
set_binding('line_mode', 'left', 'h')
|
||||
set_binding('line_mode', 'extend_left', 'Shift+Left')
|
||||
set_binding('line_mode', 'right', 'Right')
|
||||
set_binding('line_mode', 'right', 'l')
|
||||
set_binding('line_mode', 'extend_right', 'Shift+Right')
|
||||
set_binding('line_mode', 'page_up', 'Page_Up')
|
||||
set_binding('line_mode', 'page_up', 'Ctrl+u')
|
||||
set_binding('line_mode', 'extend_page_up', 'Shift+Page_Up')
|
||||
set_binding('line_mode', 'extend_page_up', 'Shift+Ctrl+U')
|
||||
set_binding('line_mode', 'page_down', 'Page_Down')
|
||||
set_binding('line_mode', 'page_down', 'Ctrl+d')
|
||||
set_binding('line_mode', 'extend_page_down', 'Shift+Page_Down')
|
||||
set_binding('line_mode', 'extend_page_down', 'Shift+Ctrl+D')
|
||||
set_binding('line_mode', 'delete_text', 'BackSpace')
|
||||
set_binding('line_mode', 'delete_text', 'Delete')
|
||||
set_binding('line_mode', 'delete_text', 'x')
|
||||
set_binding('line_mode', 'clear_edits', 'r')
|
||||
set_binding('line_mode', 'isolate', 'i')
|
||||
set_binding('line_mode', 'first_difference', 'Ctrl+Home')
|
||||
set_binding('line_mode', 'first_difference', 'Shift+P')
|
||||
set_binding('line_mode', 'previous_difference', 'p')
|
||||
set_binding('line_mode', 'next_difference', 'n')
|
||||
set_binding('line_mode', 'last_difference', 'Ctrl+End')
|
||||
set_binding('line_mode', 'last_difference', 'Shift+N')
|
||||
#set_binding('line_mode', 'copy_selection_right', 'Shift+L')
|
||||
#set_binding('line_mode', 'copy_selection_left', 'Shift+H')
|
||||
set_binding('line_mode', 'copy_left_into_selection', 'Shift+L')
|
||||
set_binding('line_mode', 'copy_right_into_selection', 'Shift+H')
|
||||
set_binding('line_mode', 'merge_from_left_then_right', 'm')
|
||||
set_binding('line_mode', 'merge_from_right_then_left', 'Shift+M')
|
||||
set_binding('align_mode', 'enter_line_mode', 'Escape')
|
||||
set_binding('align_mode', 'align', 'space')
|
||||
set_binding('align_mode', 'enter_character_mode', 'Return')
|
||||
set_binding('align_mode', 'enter_character_mode', 'KP_Enter')
|
||||
set_binding('align_mode', 'first_line', 'g')
|
||||
set_binding('align_mode', 'last_line', 'Shift+G')
|
||||
set_binding('align_mode', 'up', 'Up')
|
||||
set_binding('align_mode', 'up', 'k')
|
||||
set_binding('align_mode', 'down', 'Down')
|
||||
set_binding('align_mode', 'down', 'j')
|
||||
set_binding('align_mode', 'left', 'Left')
|
||||
set_binding('align_mode', 'left', 'h')
|
||||
set_binding('align_mode', 'right', 'Right')
|
||||
set_binding('align_mode', 'right', 'l')
|
||||
set_binding('align_mode', 'page_up', 'Page_Up')
|
||||
set_binding('align_mode', 'page_up', 'Ctrl+u')
|
||||
set_binding('align_mode', 'page_down', 'Page_Down')
|
||||
set_binding('align_mode', 'page_down', 'Ctrl+d')
|
||||
set_binding('character_mode', 'enter_line_mode', 'Escape')
|
||||
|
||||
# default colours
|
||||
self.colours = {
|
||||
'alignment' : _Colour(1.0, 1.0, 0.0),
|
||||
'character_selection' : _Colour(0.7, 0.7, 1.0),
|
||||
'cursor' : _Colour(0.0, 0.0, 0.0),
|
||||
'difference_1' : _Colour(1.0, 0.625, 0.625),
|
||||
'difference_2' : _Colour(0.85, 0.625, 0.775),
|
||||
'difference_3' : _Colour(0.85, 0.775, 0.625),
|
||||
'hatch' : _Colour(0.8, 0.8, 0.8),
|
||||
'line_number' : _Colour(0.0, 0.0, 0.0),
|
||||
'line_number_background' : _Colour(0.75, 0.75, 0.75),
|
||||
'line_selection' : _Colour(0.7, 0.7, 1.0),
|
||||
'map_background' : _Colour(0.6, 0.6, 0.6),
|
||||
'margin' : _Colour(0.8, 0.8, 0.8),
|
||||
'edited' : _Colour(0.5, 1.0, 0.5),
|
||||
'preedit' : _Colour(0.0, 0.0, 0.0),
|
||||
'text' : _Colour(0.0, 0.0, 0.0),
|
||||
'text_background' : _Colour(1.0, 1.0, 1.0) }
|
||||
|
||||
# default floats
|
||||
self.floats = {
|
||||
'alignment_opacity' : 1.0,
|
||||
'character_difference_opacity' : 0.4,
|
||||
'character_selection_opacity' : 0.4,
|
||||
'edited_opacity' : 0.4,
|
||||
'line_difference_opacity' : 0.3,
|
||||
'line_selection_opacity' : 0.4 }
|
||||
|
||||
# default strings
|
||||
self.strings = {}
|
||||
|
||||
# syntax highlighting support
|
||||
self.syntaxes = {}
|
||||
self.syntax_file_patterns = {}
|
||||
self.syntax_magic_patterns = {}
|
||||
self.current_syntax = None
|
||||
|
||||
# list of imported resources files (we only import each file once)
|
||||
self.resource_files = set()
|
||||
|
||||
# special string resources
|
||||
self.setDifferenceColours('difference_1 difference_2 difference_3')
|
||||
|
||||
# keyboard action processing
|
||||
def setKeyBinding(self, ctx, s, v):
|
||||
action_tuple = (ctx, s)
|
||||
modifiers = Gdk.ModifierType(0)
|
||||
key = None
|
||||
for token in v.split('+'):
|
||||
if token == 'Shift':
|
||||
modifiers |= Gdk.ModifierType.SHIFT_MASK
|
||||
elif token == 'Ctrl':
|
||||
modifiers |= Gdk.ModifierType.CONTROL_MASK
|
||||
elif token == 'Alt':
|
||||
modifiers |= Gdk.ModifierType.MOD1_MASK # pylint: disable=no-member
|
||||
elif len(token) == 0 or token[0] == '_':
|
||||
raise ValueError()
|
||||
else:
|
||||
token = 'KEY_' + token
|
||||
if not hasattr(Gdk, token):
|
||||
raise ValueError()
|
||||
key = getattr(Gdk, token)
|
||||
if key is None:
|
||||
raise ValueError()
|
||||
key_tuple = (ctx, (key, modifiers))
|
||||
|
||||
# remove any existing binding
|
||||
if key_tuple in self.keybindings_lookup:
|
||||
self._removeKeyBinding(key_tuple)
|
||||
|
||||
# ensure we have a set to hold this action
|
||||
if action_tuple not in self.keybindings:
|
||||
self.keybindings[action_tuple] = {}
|
||||
bindings = self.keybindings[action_tuple]
|
||||
|
||||
# menu items can only have one binding
|
||||
if ctx == 'menu':
|
||||
for k in bindings.keys():
|
||||
self._removeKeyBinding(k)
|
||||
|
||||
# add the binding
|
||||
bindings[key_tuple] = None
|
||||
self.keybindings_lookup[key_tuple] = action_tuple
|
||||
|
||||
def _removeKeyBinding(self, key_tuple):
|
||||
action_tuple = self.keybindings_lookup[key_tuple]
|
||||
del self.keybindings_lookup[key_tuple]
|
||||
del self.keybindings[action_tuple][key_tuple]
|
||||
|
||||
def getActionForKey(self, ctx, key, modifiers):
|
||||
try:
|
||||
return self.keybindings_lookup[(ctx, (key, modifiers))][1]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def getKeyBindings(self, ctx, s):
|
||||
try:
|
||||
return [ t for c, t in self.keybindings[(ctx, s)].keys() ]
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
# colours used for indicating differences
|
||||
def setDifferenceColours(self, s):
|
||||
colours = s.split()
|
||||
if len(colours) > 0:
|
||||
self.difference_colours = colours
|
||||
|
||||
def getDifferenceColour(self, i):
|
||||
n = len(self.difference_colours)
|
||||
return self.getColour(self.difference_colours[(i + n - 1) % n])
|
||||
|
||||
# colour resources
|
||||
def getColour(self, symbol):
|
||||
try:
|
||||
return self.colours[symbol]
|
||||
except KeyError:
|
||||
utils.logDebug(f'Warning: unknown colour "{symbol}"')
|
||||
self.colours[symbol] = v = _Colour(0.0, 0.0, 0.0)
|
||||
return v
|
||||
|
||||
# float resources
|
||||
def getFloat(self, symbol):
|
||||
try:
|
||||
return self.floats[symbol]
|
||||
except KeyError:
|
||||
utils.logDebug(f'Warning: unknown float "{symbol}"')
|
||||
self.floats[symbol] = v = 0.5
|
||||
return v
|
||||
|
||||
# string resources
|
||||
def getString(self, symbol):
|
||||
try:
|
||||
return self.strings[symbol]
|
||||
except KeyError:
|
||||
utils.logDebug(f'Warning: unknown string "{symbol}"')
|
||||
self.strings[symbol] = v = ''
|
||||
return v
|
||||
|
||||
# syntax highlighting
|
||||
def getSyntaxNames(self):
|
||||
return list(self.syntaxes.keys())
|
||||
|
||||
def getSyntax(self, name):
|
||||
return self.syntaxes.get(name, None)
|
||||
|
||||
def guessSyntaxForFile(self, name, ss):
|
||||
name = os.path.basename(name)
|
||||
for key, pattern in self.syntax_file_patterns.items():
|
||||
if pattern.search(name):
|
||||
return key
|
||||
# fallback to analysing the first line of the file
|
||||
if len(ss) > 0:
|
||||
s = ss[0]
|
||||
for key, pattern in self.syntax_magic_patterns.items():
|
||||
if pattern.search(s):
|
||||
return key
|
||||
return None
|
||||
|
||||
# parse resource files
|
||||
def parse(self, file_name):
|
||||
# only process files once
|
||||
if file_name in self.resource_files:
|
||||
return
|
||||
|
||||
self.resource_files.add(file_name)
|
||||
with open(file_name, 'r', encoding='utf-8') as f:
|
||||
ss = utils.readconfiglines(f)
|
||||
|
||||
# FIXME: improve validation
|
||||
for i, s in enumerate(ss):
|
||||
args = shlex.split(s, True)
|
||||
if len(args) == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
# eg. add Python syntax highlighting:
|
||||
# import /usr/share/diffuse/syntax/python.syntax
|
||||
if args[0] == 'import' and len(args) == 2:
|
||||
path = os.path.expanduser(args[1])
|
||||
# relative paths are relative to the parsed file
|
||||
path = os.path.join(utils.globEscape(os.path.dirname(file_name)), path)
|
||||
paths = glob.glob(path)
|
||||
if len(paths) == 0:
|
||||
paths = [ path ]
|
||||
for path in paths:
|
||||
# convert to absolute path so the location of
|
||||
# any processing errors are reported with
|
||||
# normalised file names
|
||||
self.parse(os.path.abspath(path))
|
||||
# eg. make Ctrl+o trigger the open_file menu item
|
||||
# keybinding menu open_file Ctrl+o
|
||||
elif args[0] == 'keybinding' and len(args) == 4:
|
||||
self.setKeyBinding(args[1], args[2], args[3])
|
||||
# eg. set the regular background colour to white
|
||||
# colour text_background 1.0 1.0 1.0
|
||||
elif args[0] in [ 'colour', 'color' ] and len(args) == 5:
|
||||
self.colours[args[1]] = _Colour(float(args[2]), float(args[3]), float(args[4]))
|
||||
# eg. set opacity of the line_selection colour
|
||||
# float line_selection_opacity 0.4
|
||||
elif args[0] == 'float' and len(args) == 3:
|
||||
self.floats[args[1]] = float(args[2])
|
||||
# eg. set the help browser
|
||||
# string help_browser gnome-help
|
||||
elif args[0] == 'string' and len(args) == 3:
|
||||
self.strings[args[1]] = args[2]
|
||||
if args[1] == 'difference_colours':
|
||||
self.setDifferenceColours(args[2])
|
||||
# eg. start a syntax specification for Python
|
||||
# syntax Python normal text
|
||||
# where 'normal' is the name of the default state and
|
||||
# 'text' is the classification of all characters not
|
||||
# explicitly matched by a syntax highlighting rule
|
||||
elif args[0] == 'syntax' and (len(args) == 2 or len(args) == 4):
|
||||
key = args[1]
|
||||
if len(args) == 2:
|
||||
# remove file pattern for a syntax specification
|
||||
try:
|
||||
del self.syntax_file_patterns[key]
|
||||
except KeyError:
|
||||
pass
|
||||
# remove magic pattern for a syntax specification
|
||||
try:
|
||||
del self.syntax_magic_patterns[key]
|
||||
except KeyError:
|
||||
pass
|
||||
# remove a syntax specification
|
||||
self.current_syntax = None
|
||||
try:
|
||||
del self.syntaxes[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.current_syntax = _SyntaxParser(args[2], args[3])
|
||||
self.syntaxes[key] = self.current_syntax
|
||||
# eg. transition from state 'normal' to 'comment' when
|
||||
# the pattern '#' is matched and classify the matched
|
||||
# characters as 'python_comment'
|
||||
# syntax_pattern normal comment python_comment '#'
|
||||
elif (
|
||||
args[0] == 'syntax_pattern' and
|
||||
self.current_syntax is not None and
|
||||
len(args) >= 5
|
||||
):
|
||||
flags = 0
|
||||
for arg in args[5:]:
|
||||
if arg == 'ignorecase':
|
||||
flags |= re.IGNORECASE
|
||||
else:
|
||||
raise ValueError()
|
||||
self.current_syntax.addPattern(
|
||||
args[1],
|
||||
args[2],
|
||||
args[3],
|
||||
re.compile(args[4], flags))
|
||||
# eg. default to the Python syntax rules when viewing
|
||||
# a file ending with '.py' or '.pyw'
|
||||
# syntax_files Python '\.pyw?$'
|
||||
elif args[0] == 'syntax_files' and (len(args) == 2 or len(args) == 3):
|
||||
key = args[1]
|
||||
if len(args) == 2:
|
||||
# remove file pattern for a syntax specification
|
||||
try:
|
||||
del self.syntax_file_patterns[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
flags = 0
|
||||
if utils.isWindows():
|
||||
flags |= re.IGNORECASE
|
||||
self.syntax_file_patterns[key] = re.compile(args[2], flags)
|
||||
# eg. default to the Python syntax rules when viewing
|
||||
# a files starting with patterns like #!/usr/bin/python
|
||||
# syntax_magic Python '^#!/usr/bin/python$'
|
||||
elif args[0] == 'syntax_magic' and len(args) > 1:
|
||||
key = args[1]
|
||||
if len(args) == 2:
|
||||
# remove magic pattern for a syntax specification
|
||||
try:
|
||||
del self.syntax_magic_patterns[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
flags = 0
|
||||
for arg in args[3:]:
|
||||
if arg == 'ignorecase':
|
||||
flags |= re.IGNORECASE
|
||||
else:
|
||||
raise ValueError()
|
||||
self.syntax_magic_patterns[key] = re.compile(args[2], flags)
|
||||
else:
|
||||
raise ValueError()
|
||||
# pylint: disable-next=bare-except
|
||||
except: # Grr... the 're' module throws weird errors
|
||||
#except ValueError:
|
||||
utils.logError(_(f'Error processing line {i + 1} of {file_name}.'))
|
||||
|
||||
# colour resources
|
||||
class _Colour:
|
||||
def __init__(self, r, g, b, a=1.0):
|
||||
# the individual colour components as floats in the range [0, 1]
|
||||
self.red = r
|
||||
self.green = g
|
||||
self.blue = b
|
||||
self.alpha = a
|
||||
|
||||
# multiply by scalar
|
||||
def __mul__(self, s):
|
||||
return _Colour(s * self.red, s * self.green, s * self.blue, s * self.alpha)
|
||||
|
||||
# add colours
|
||||
def __add__(self, other):
|
||||
return _Colour(
|
||||
self.red + other.red,
|
||||
self.green + other.green,
|
||||
self.blue + other.blue,
|
||||
self.alpha + other.alpha)
|
||||
|
||||
# over operator
|
||||
def over(self, other):
|
||||
return self + other * (1 - self.alpha)
|
||||
|
||||
# class to build and run a finite state machine for identifying syntax tokens
|
||||
class _SyntaxParser:
|
||||
# create a new state machine that begins in initial_state and classifies
|
||||
# all characters not matched by the patterns as default_token_type
|
||||
def __init__(self, initial_state, default_token_type):
|
||||
# initial state for the state machine when parsing a new file
|
||||
self.initial_state = initial_state
|
||||
# default classification of characters that are not explicitly matched
|
||||
# by any state transition patterns
|
||||
self.default_token_type = default_token_type
|
||||
# mappings from a state to a list of (pattern, token_type, next_state)
|
||||
# tuples indicating the new state for the state machine when 'pattern'
|
||||
# is matched and how to classify the matched characters
|
||||
self.transitions_lookup = { initial_state : [] }
|
||||
|
||||
# Adds a new edge to the finite state machine from prev_state to
|
||||
# next_state. Characters will be identified as token_type when pattern is
|
||||
# matched. Any newly referenced state will be added. Patterns for edges
|
||||
# leaving a state will be tested in the order they were added to the finite
|
||||
# state machine.
|
||||
def addPattern(self, prev_state, next_state, token_type, pattern):
|
||||
lookup = self.transitions_lookup
|
||||
for state in prev_state, next_state:
|
||||
if state not in lookup:
|
||||
lookup[state] = []
|
||||
lookup[prev_state].append([pattern, token_type, next_state])
|
||||
|
||||
# given a string and an initial state, identify the final state and tokens
|
||||
def parse(self, state_name, s):
|
||||
lookup = self.transitions_lookup
|
||||
transitions, blocks, start = lookup[state_name], [], 0
|
||||
while start < len(s):
|
||||
for pattern, token_type, next_state in transitions:
|
||||
m = pattern.match(s, start)
|
||||
if m is not None:
|
||||
end, state_name = m.span()[1], next_state
|
||||
transitions = lookup[state_name]
|
||||
break
|
||||
else:
|
||||
end, token_type = start + 1, self.default_token_type
|
||||
if len(blocks) > 0 and blocks[-1][2] == token_type:
|
||||
blocks[-1][1] = end
|
||||
else:
|
||||
blocks.append([start, end, token_type])
|
||||
start = end
|
||||
return state_name, blocks
|
40
src/utils.py
40
src/utils.py
|
@ -47,6 +47,29 @@ class MessageDialog(Gtk.MessageDialog):
|
|||
text=s)
|
||||
self.set_title(constants.APP_NAME)
|
||||
|
||||
# widget to help pick an encoding
|
||||
class EncodingMenu(Gtk.Box):
|
||||
def __init__(self, prefs, autodetect=False):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.combobox = combobox = Gtk.ComboBoxText.new()
|
||||
self.encodings = prefs.getEncodings()[:]
|
||||
for e in self.encodings:
|
||||
combobox.append_text(e)
|
||||
if autodetect:
|
||||
self.encodings.insert(0, None)
|
||||
combobox.prepend_text(_('Auto Detect'))
|
||||
self.pack_start(combobox, False, False, 0) # pylint: disable=no-member
|
||||
combobox.show()
|
||||
|
||||
def set_text(self, encoding):
|
||||
encoding = norm_encoding(encoding)
|
||||
if encoding in self.encodings:
|
||||
self.combobox.set_active(self.encodings.index(encoding))
|
||||
|
||||
def get_text(self):
|
||||
i = self.combobox.get_active()
|
||||
return self.encodings[i] if i >= 0 else None
|
||||
|
||||
# platform test
|
||||
def isWindows():
|
||||
return os.name == 'nt'
|
||||
|
@ -69,7 +92,7 @@ def logError(msg):
|
|||
def logErrorAndDialog(msg,parent=None):
|
||||
logError(msg)
|
||||
dialog = MessageDialog(parent, Gtk.MessageType.ERROR, msg)
|
||||
dialog.run()
|
||||
dialog.run() # pylint: disable=no-member
|
||||
dialog.destroy()
|
||||
|
||||
# create nested subdirectories and return the complete path
|
||||
|
@ -190,6 +213,9 @@ def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
|
|||
return _strip_eols(splitlines(popenRead(
|
||||
dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore')))
|
||||
|
||||
def readconfiglines(fd):
|
||||
return fd.read().replace('\r', '').split('\n')
|
||||
|
||||
# escape special glob characters
|
||||
def globEscape(s):
|
||||
m = { c: f'[{c}]' for c in '[]?*' }
|
||||
|
@ -227,6 +253,18 @@ def splitlines(text: str) -> list[str]:
|
|||
def readlines(fd):
|
||||
return _strip_eols(splitlines(fd.read()))
|
||||
|
||||
# map an encoding name to its standard form
|
||||
def norm_encoding(e):
|
||||
if e is not None:
|
||||
return e.replace('-', '_').lower()
|
||||
return None
|
||||
|
||||
def null_to_empty(s):
|
||||
if s is None:
|
||||
s = ''
|
||||
return s
|
||||
|
||||
|
||||
# use the program's location as a starting place to search for supporting files
|
||||
# such as icon and help documentation
|
||||
if hasattr(sys, 'frozen'):
|
||||
|
|
Loading…
Reference in New Issue