1
# -*- coding: utf-8 -*-
3
# Kate/Pâté color plugins
4
# Copyright 2010-2013 by Alex Turbov <i.zaufi@gmail.com>
5
# Copyright 2013 by Phil Schaf
8
# This software is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Lesser General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
13
# This software is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU Lesser General Public License for more details.
18
# You should have received a copy of the GNU Lesser General Public License
19
# along with this software. If not, see <http://www.gnu.org/licenses/>.
22
# Here is a short list of plugins in this file:
24
# Insert Color (Meta+Shift+C)
25
# open color choose dialog and insert selected color as #hex string
29
# Select a color string of any kind to display its color in a tooltip
32
'''Utilities to work with hexadecimal colors in documents
34
Shows a preview of a selected hexadecimal color string (e.g. #fe57a1) in a tooltip
35
and/or all parsed hexadecimal colors in a 'Palette' tool view, ability to edit/insert
36
hexadecimal color strings.
45
from PyQt4.QtCore import QEvent, QObject, QTimer, Qt, pyqtSlot, pyqtSignal
46
from PyQt4.QtGui import QColor, QFrame, QPalette, QToolTip, QWidget, QVBoxLayout
48
from PyKDE4.kdecore import i18nc
49
from PyKDE4.kdeui import KColorDialog, KColorCells, KPushButton
50
from PyKDE4.ktexteditor import KTextEditor
54
from libkatepate import common
57
_INSERT_COLOR_LCC = 'insertColor:lastUsedColor'
58
_CELLS_COUNT_PER_ROW = 5
61
colorChooserWidget = None
64
def _calc_dimensions_for_items_count(count):
65
'''Recalculate rows/columns of table view for given items count'''
66
rows = int(math.sqrt(count))
67
columns = int(count / rows) + int(bool(count % rows))
68
return (rows, columns)
71
def _set_tooltips(rows, columns, colors):
72
'''Set tool tips for color cells in a KColorCells widget'''
74
for r in range(0, rows):
75
for c in range(0, columns):
76
item = colors.item(r, c)
80
color = item.data(Qt.BackgroundRole)
82
item.setToolTip(color.name())
87
class ColorChooser(QFrame):
88
'''Completion-like widget to quick select hexadecimal colors used in a document'''
89
colorSelected = pyqtSignal(QColor)
91
def __init__(self, parent):
92
super(ColorChooser, self).__init__(parent)
93
self.colors = KColorCells(self, 1, 1)
94
self.colors.setAcceptDrags(False)
95
self.colors.setEditTriggers(self.colors.NoEditTriggers)
96
self.otherBtn = KPushButton(self)
97
self.otherBtn.setText(i18nc('@action:button', '&Other...'))
98
layout = QVBoxLayout(self)
99
layout.addWidget(self.colors)
100
layout.addWidget(self.otherBtn)
101
self.setFocusPolicy(Qt.StrongFocus)
102
self.setFrameShape(QFrame.Panel)
103
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup);
105
# Subscribe to observe widget events
106
# - open KColorDialog on 'Other...' button click
107
self.otherBtn.clicked.connect(self.show_color_dialog)
108
# - select color by RMB click or ENTER on keyboard
109
self.colors.cellActivated.connect(self.color_selected)
111
self.installEventFilter(self)
114
def setColors(self, colors):
116
rows, columns = _calc_dimensions_for_items_count(len(colors))
119
self.show_color_dialog(True)
121
print('ColorUtils: colors={}, recalc size: rows={}, columns={}'.format(len(colors), rows, columns))
122
self.colors.setColumnCount(columns)
123
self.colors.setRowCount(rows)
124
print('ColorUtils: before: recalc size: ww={}, wh={}'.format(self.width(), self.height()))
125
self.colors.setMinimumSize(20 * columns, 25 * rows)
126
self.updateGeometry()
127
#self.resize(20 * columns + 30, 25 * rows + 18)
129
print('ColorUtils: after: recalc size: ww={}, wh={}'.format(self.width(), self.height()))
130
self.colors.resizeColumnsToContents()
131
self.colors.resizeRowsToContents()
133
for i, color in enumerate(colors):
134
self.colors.setColor(i, color)
135
_set_tooltips(rows, columns, self.colors) # Set tooltips for all valid color cells
136
self.colors.setFocus() # Give a focus to widget
137
self.show() # Show it!
141
def show_color_dialog(self, f):
142
'''Get color using KColorDialog'''
143
print('ColorUtils: Dialog requested')
144
# Preselect last used color
145
color = QColor(kate.configuration[_INSERT_COLOR_LCC])
146
result = KColorDialog.getColor(color)
147
if result == KColorDialog.Accepted: # Did user press OK?
148
self.emitSelectedColorHideSelf(color)
152
def color_selected(self, row, column):
153
'''Smth has selected in KColorCells'''
154
color = self.colors.item(row, column).data(Qt.BackgroundRole)
155
print('ColorUtils: activated row={}, column={}, color={}'.format(row, column, color))
156
self.emitSelectedColorHideSelf(color)
159
def emitSelectedColorHideSelf(self, color):
160
# Remember last selected color for future preselect
161
kate.configuration[_INSERT_COLOR_LCC] = color.name()
162
self.colorSelected.emit(color)
166
def eventFilter(self, obj, event):
167
'''Hide self on Esc key'''
168
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Escape:
171
return super(ColorChooser, self).eventFilter(obj, event)
174
def moveAround(self, position):
175
'''Smart positioning self around cursor'''
176
print('ColorUtils: cursor position={}'.format(position))
177
print('ColorUtils: kwm.geo={}'.format(kate.mainWindow().geometry()))
178
print('ColorUtils: kwm.geo.center={}'.format(kate.mainWindow().geometry().center()))
179
print('ColorUtils: p={}'.format(kate.mainWindow().geometry().topLeft() + position))
180
self.colors.resizeColumnsToContents()
181
self.colors.resizeRowsToContents()
182
mwg = kate.mainWindow().geometry()
184
pos = mwg.topLeft() + position
185
pos.setY(pos.y() + 40)
187
pos.setY(pos.y() - self.height())
189
pos.setX(pos.x() - self.width())
194
def _collect_colors(document):
195
'''Scan a given document and collect unique colors
197
Returns a list of QColor objects.
200
# Iterate over document's lines trying to find #colors
201
for l in range(0, document.lines()):
202
line = document.line(l) # Get the current line
203
start = 0 # Set initial position to 0 (line start)
204
while start < len(line): # Repeat 'till the line end
205
start = line.find('#', start) # Try to find a '#' character (start of #color)
206
if start == -1: # Did we found smth?
207
break # No! Nothing to do...
208
# Try to get a word right after the '#' char
211
if not (c in string.hexdigits or c in string.ascii_letters):
214
color_range = KTextEditor.Range(l, start, l, end)
215
color_str = document.text(color_range)
216
color = QColor(color_str)
217
if color.isValid() and color not in result:
219
print('ColorUtils: scan for #colors found {}'.format(color_str))
224
def _get_color_range_under_cursor(view):
225
assert(view is not None)
226
if view.selection(): # Some text selected, just use it as input...
227
color_range = view.selectionRange()
228
else: # If no selection, try to get a #color under cursor
229
color_range = common.getBoundTextRangeSL(
230
common.IDENTIFIER_BOUNDARIES - {'#'}
231
, common.IDENTIFIER_BOUNDARIES
232
, view.cursorPosition()
235
# Check if a word under cursor is a valid #color
236
color = QColor(view.document().text(color_range))
237
if not color.isValid():
238
color_range = KTextEditor.Range(view.cursorPosition(), view.cursorPosition())
242
@kate.action(i18nc('@action:inmenu', 'Insert Color'), shortcut='Meta+Shift+C', icon='color', menu='Tools')
244
'''Insert/edit color string using color chooser dialog
246
If cursor positioned in a color string, this action will edit it, otherwise
247
a new color string will be inserted into a document.
249
document = kate.activeDocument()
250
view = kate.activeView()
252
global colorChooserWidget
253
colorChooserWidget.setColors(_collect_colors(document))
254
colorChooserWidget.moveAround(view.cursorPositionCoordinates())
258
def _insertColorIntoActiveDocument(color):
259
color_str = color.name() # Get it as color string
260
print('ColorUtils: selected color: {}'.format(color_str))
261
document = kate.activeDocument()
262
view = kate.activeView()
263
cursor = view.cursorPosition()
264
has_selection = view.selection() # Remember current selection state
265
color_range = _get_color_range_under_cursor(view)
266
document.startEditing()
267
document.replaceText(color_range, color_str) # Replace selected/found range w/ a new text
268
document.endEditing()
269
# Select just entered #color, if something was selected before
271
start_pos = color_range.start()
272
view.setSelection(KTextEditor.Range(start_pos, len(color_str)))
276
'''Class encapsuling the ability to show color swatches (colored tooltips)'''
277
swatch_template = '<div>{}</div>'.format(' ' * 10)
280
self.old_palette = None
281
kate.viewChanged(self.view_changed)
284
def view_changed(self):
285
'''Connects a swatch showing slot to each view’s selection change signal'''
286
view = kate.activeView()
288
view.selectionChanged.connect(self.show_swatch)
290
def show_swatch(self, view):
291
'''Shows the swatch if a valid color is selected'''
293
color = QColor(view.selectionText())
295
cursor_pos = view.cursorPositionCoordinates()
296
QToolTip.showText(cursor_pos, self.swatch_template)
297
self.change_palette(color)
299
def change_palette(self, color):
300
'''Sets the global tooltip background to the given color and initializes reset'''
301
self.old_palette = QToolTip.palette()
302
p = QPalette(self.old_palette)
303
p.setColor(QPalette.All, QPalette.ToolTipBase, color)
304
QToolTip.setPalette(p)
306
self.timer = QTimer()
307
self.timer.timeout.connect(self.try_reset_palette)
308
self.timer.start(300) #short enough not to flicker a wrongly colored tooltip
310
def try_reset_palette(self):
311
'''Resets the global tooltip background color as soon as the swatch is hidden'''
312
if self.old_palette is not None and not QToolTip.isVisible():
313
QToolTip.setPalette(self.old_palette)
314
self.old_palette = None
317
class ColorRangePair:
318
'''Simple class to store a #color associated w/ a location in a document'''
322
def __init__(self, color, color_range):
324
self.color_range = color_range
327
class PaletteView(QObject):
328
'''A toolview to display palette of the current document'''
331
colorCellsWidget = None
333
def __init__(self, parent):
334
super(PaletteView, self).__init__(parent)
335
self.toolView = kate.mainInterfaceWindow().createToolView(
336
'color_tools_pate_plugin'
337
, kate.Kate.MainWindow.Bottom
338
, kate.gui.loadIcon('color')
339
, i18nc('@title:tab', 'Palette')
341
self.toolView.installEventFilter(self)
342
# By default, the toolview has box layout, which is not easy to delete.
343
# For now, just add an extra widget.
344
top = QWidget(self.toolView)
345
# Set up the user interface from Designer.
346
interior = uic.loadUi(os.path.join(os.path.dirname(__file__), 'color_tools_toolview.ui'), top)
347
interior.update.clicked.connect(self.update)
348
self.colorCellsWidget = KColorCells(interior, 1, 1)
349
# TODO Don't know how to deal w/ drag-n-drops :(
350
# It seems there is no signal to realize that some item has changed :(
351
# (at lieast I didn't find it)
352
self.colorCellsWidget.setAcceptDrags(False)
353
self.colorCellsWidget.setEditTriggers(self.colorCellsWidget.NoEditTriggers)
354
interior.verticalLayout.addWidget(self.colorCellsWidget)
355
self.colorCellsWidget.colorSelected.connect(self.colorSelected)
356
self.colorCellsWidget.colorDoubleClicked.connect(self.colorDoubleClicked)
359
'''Plugins that use a toolview need to delete it for reloading to work.'''
361
self.toolView.deleteLater()
364
def eventFilter(self, obj, event):
365
'''Hide the Palette tool view on ESCAPE key'''
366
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Escape:
367
kate.mainInterfaceWindow().hideToolView(self.toolView)
369
return self.toolView.eventFilter(obj, event)
371
def updateColors(self, view=None):
372
'''Scan a document for #colors
374
Returns a list of tuples: QColor and range in a document
375
TODO Some refactoring needed to reduce code duplication
376
(@sa _get_color_range_under_cursor())
378
self.colors = [] # Clear previous colors
380
document = view.document()
383
document = kate.activeDocument()
384
except kate.NoActiveView:
385
return # Do nothing if we can't get a current document
386
# Iterate over document's lines trying to find #colors
387
for l in range(0, document.lines()):
388
line = document.line(l) # Get the current line
389
start = 0 # Set initial position to 0 (line start)
390
while start < len(line): # Repeat 'till the line end
391
start = line.find('#', start) # Try to find a '#' character (start of #color)
392
if start == -1: # Did we found smth?
393
break # No! Nothing to do...
394
# Try to get a word right after the '#' char
397
if not (c in string.hexdigits):
400
color_range = KTextEditor.Range(l, start, l, end)
401
color_str = document.text(color_range)
402
color = QColor(color_str)
404
self.colors.append(ColorRangePair(color, color_range))
405
print('ColorUtilsToolView: scan for #colors found {}'.format(color_str))
408
def updateColorCells(self):
409
'''Calculate rows*columns and fill the cells w/ #colors'''
411
print('ColorToolView: colors={}'.format(len(self.colors)))
412
# Recalculate rows/columns
413
print('ColorToolView: width={}, heigth={}'.format(self.colorCellsWidget.width(), self.colorCellsWidget.height()))
414
columns = int(self.colorCellsWidget.width() / 30)
415
print('ColorToolView: columns={}'.format(columns))
416
visible_rows = int(self.colorCellsWidget.height() / 25)
417
print('ColorToolView: visible_rows={}'.format(visible_rows))
418
if len(self.colors) < (columns * visible_rows):
420
print('ColorToolView: rows={}'.format(rows))
422
visible_cells = columns * visible_rows
423
print('ColorToolView: visible_cells={}'.format(visible_cells))
424
rest = len(self.colors) - visible_cells
425
print('ColorToolView: rest={}'.format(rest))
426
rows = visible_rows + int(rest / columns) + int(bool(rest % columns))
427
print('ColorToolView: rows={}'.format(rows))
431
self.colors.append(ColorRangePair(QColor(), KTextEditor.Range()))
432
self.colorCellsWidget.setColumnCount(columns)
433
self.colorCellsWidget.setRowCount(rows)
434
self.colorCellsWidget.resizeColumnsToContents()
435
self.colorCellsWidget.resizeRowsToContents()
437
for i, crp in enumerate(self.colors):
438
self.colorCellsWidget.setColor(i, crp.color)
439
for i in range(len(self.colors), columns * rows):
440
self.colorCellsWidget.setColor(i, QColor())
441
_set_tooltips(rows, columns, self.colorCellsWidget)
445
def update(self, view=None):
446
self.updateColors(view)
447
self.updateColorCells()
450
@pyqtSlot(int, QColor)
451
def colorSelected(self, idx, color):
452
'''Move cursor to the position of the selected #color and select the range'''
453
view = kate.activeView()
454
view.setCursorPosition(self.colors[idx].color_range.start())
455
view.setSelection(self.colors[idx].color_range)
458
@pyqtSlot(int, QColor)
459
def colorDoubleClicked(self, idx, color):
460
'''Edit selected color on double click'''
467
def viewChanged(view=None):
468
''' Rescan current document on view create and/or change'''
470
return paletteView.update(view)
475
'''Iniialize global variables and read config'''
476
# Set default value for last used #color if not configured yet
477
if _INSERT_COLOR_LCC not in kate.configuration:
478
kate.configuration[_INSERT_COLOR_LCC] = '#ffffff'
480
swatcher = ColorSwatcher()
482
# Make an instance of a palette tool view
484
if paletteView is None:
485
paletteView = PaletteView(kate.mainWindow())
487
# Make an instance of a color chooser widget,
488
# connect it to active document updater
489
global colorChooserWidget
490
if colorChooserWidget is None:
491
colorChooserWidget = ColorChooser(kate.mainWindow())
492
colorChooserWidget.colorSelected.connect(_insertColorIntoActiveDocument)
497
'''Plugins that use a toolview need to delete it for reloading to work.'''
503
global colorChooserWidget
504
if colorChooserWidget:
505
del colorChooserWidget
506
colorChooserWidget = None
509
# kate: space-indent on; indent-width 4;