This commit is contained in:
emdee 2024-02-11 07:50:55 +00:00
parent c7580c3a35
commit 60c48bcb6a
12 changed files with 652 additions and 532 deletions

View File

@ -16,12 +16,22 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
# #
# to run the tests, run make PASS=controllerpassword test
PREFIX=/usr/local
PYTHON_EXE_MSYS=${PREFIX}/bin/python3.sh
PIP_EXE_MSYS=${PREFIX}/bin/pip3.sh
LOCAL_DOCTEST=${PREFIX}/bin/toxcore_run_doctest3.bash
DOCTEST=${LOCAL_DOCTEST}
PYTHON_MINOR=`python3 --version 2>&1 | sed -e 's@^.* @@' -e 's@\.[0-9]*$$@@'`
MOD=qweechat
all: check all: check
check: lint test check: lint test
sh ${PYTHON_EXE_MSYS} -c "import ${MOD}"
lint: flake8 pylint bandit lint:: flake8 pylint bandit
flake8: flake8:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
@ -32,3 +42,8 @@ pylint:
bandit: bandit:
bandit -r qweechat bandit -r qweechat
install::
# we install --nodeps because pip is installing stuff we already have in the OS
${PYTHON_EXE_MSYS} setup.py install \
--prefix ${PREFIX}/

51
pyproject.toml Normal file
View File

@ -0,0 +1,51 @@
[project]
name = 'qweechat'
requires-python = ">= 3.7"
description = "qtpy channel for qweechat"
keywords = ["qt", "console"]
classifiers = [
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
# Indicate who your project is intended for
"Intended Audience :: Developers",
# Specify the Python versions you support here.
"Programming Language :: Python :: 3",
"License :: OSI Approved",
"Operating System :: POSIX :: BSD :: FreeBSD",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
]
dynamic = ["version", "readme", "dependencies"] # cannot be dynamic ['license']
[project.scripts]
qweechat = "qweechat:iMain"
[project.license]
file = "COPYING"
[project.urls]
repository = "https://git.plastiras.org/emdee/qweechat"
homepage = "https://git.plastiras.org/emdee/qweechat"
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
version = {attr = "qweechat.version.VERSION"}
readme = {file = ["README.md"]}
dependencies = {file = ["requirements.txt"]}
#[tool.setuptools.packages.find]
#where = "src"

View File

@ -25,7 +25,7 @@
from pkg_resources import resource_filename from pkg_resources import resource_filename
from qtpy import QtCore, QtGui, QtWidgets from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import pyqtSignal as Signal from qtpy.QtCore import Signal
from qweechat.chat import ChatTextEdit from qweechat.chat import ChatTextEdit
from qweechat.input import InputLineEdit from qweechat.input import InputLineEdit

View File

@ -26,7 +26,7 @@ import datetime
from qtpy import QtCore, QtWidgets, QtGui from qtpy import QtCore, QtWidgets, QtGui
from qweechat import config from qweechat.config import color_options
from qweechat.weechat import color from qweechat.weechat import color
@ -62,7 +62,7 @@ class ChatTextEdit(QtWidgets.QTextEdit):
'/': True '/': True
} }
} }
self._color = color.Color(config.color_options(), self.debug) self._color = color.Color(color_options(), self.debug)
def display(self, time, prefix, text, forcecolor=None): def display(self, time, prefix, text, forcecolor=None):
if time == 0: if time == 0:

View File

@ -41,6 +41,7 @@ CONFIG_DEFAULT_OPTIONS = (('relay.hostname', '127.0.0.1'),
('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)), ('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)),
('look.debug', 'off'), ('look.debug', 'off'),
('look.statusbar', 'on')) ('look.statusbar', 'on'))
CONFIG_DEFAULT_NOTIFICATION_OPTIONS = tuple
# Default colors for WeeChat color options (option name, #rgb value) # Default colors for WeeChat color options (option name, #rgb value)
CONFIG_DEFAULT_COLOR_OPTIONS = ( CONFIG_DEFAULT_COLOR_OPTIONS = (

View File

@ -232,7 +232,7 @@ class Network(QtCore.QObject):
def is_connected(self): def is_connected(self):
"""Return True if the socket is connected, False otherwise.""" """Return True if the socket is connected, False otherwise."""
return is_state(self, at='ConnectedState') return self.is_state(at='ConnectedState')
def is_state(self, at='ConnectedState'): def is_state(self, at='ConnectedState'):
"""Return True if the socket is connected, False otherwise.""" """Return True if the socket is connected, False otherwise."""

View File

@ -2,7 +2,7 @@
# #
# preferences.py - preferences dialog box # preferences.py - preferences dialog box
# #
# Copyright (C) 2016 Ricky Brent <ricky@rickybrent.com> # Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
# #
# This file is part of QWeeChat, a Qt remote GUI for WeeChat. # This file is part of QWeeChat, a Qt remote GUI for WeeChat.
# #
@ -20,541 +20,38 @@
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
# #
import qt_compat """Preferences dialog box."""
import config
import utils
from inputlinespell import InputLineSpell
QtCore = qt_compat.import_module('QtCore') from PyQt5 import QtCore, QtWidgets as QtGui
QtGui = qt_compat.import_module('QtGui')
class PreferencesDialog(QtGui.QDialog): class PreferencesDialog(QtGui.QDialog):
"""Preferences dialog.""" """Preferences dialog."""
custom_sections = { def __init__(self, *args):
"look": "Look",
"input": "Input Box",
"nicks": "Nick List",
"buffers": "Buffer List",
"buffer_flags": False,
"notifications": "Notifications",
"color": "Colors",
"relay": "Relay/Connection"
}
def __init__(self, name, parent, *args):
QtGui.QDialog.__init__(*(self,) + args) QtGui.QDialog.__init__(*(self,) + args)
self.setModal(True) self.setModal(True)
self.setWindowTitle(name) self.setWindowTitle('Preferences')
self.parent = parent
self.config = parent.config
self.stacked_panes = QtGui.QStackedWidget()
self.list_panes = PreferencesTreeWidget("Settings")
splitter = QtGui.QSplitter() close_button = QtGui.QPushButton('Close')
splitter.addWidget(self.list_panes) close_button.pressed.connect(self.close)
splitter.addWidget(self.stacked_panes)
# Follow same order as defaults:
section_panes = {}
for section in config.CONFIG_DEFAULT_SECTIONS:
item = QtGui.QTreeWidgetItem(section)
name = section
item.setText(0, section.title())
if section in self.custom_sections:
if not self.custom_sections[section]:
continue
item.setText(0, self.custom_sections[section])
section_panes[section] = PreferencesPaneWidget(section, name)
self.list_panes.addTopLevelItem(item)
self.stacked_panes.addWidget(section_panes[section])
for setting, default in config.CONFIG_DEFAULT_OPTIONS:
section_key = setting.split(".")
section = section_key[0]
key = ".".join(section_key[1:])
section_panes[section].addItem(
key, self.config.get(section, key), default)
for key, value in self.config.items("color"):
section_panes["color"].addItem(key, value, False)
notification_field_count = len(section_panes["notifications"].fields)
notification = PreferencesNotificationBlock(
section_panes["notifications"])
section_panes["notifications"].grid.addLayout(
notification, notification_field_count, 0, 1, -1)
self.list_panes.currentItemChanged.connect(self._pane_switch)
self.list_panes.setCurrentItem(self.list_panes.topLevelItem(0))
hbox = QtGui.QHBoxLayout() hbox = QtGui.QHBoxLayout()
self.dialog_buttons = QtGui.QDialogButtonBox()
self.dialog_buttons.setStandardButtons(
QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close)
self.dialog_buttons.accepted.connect(self._save_and_close)
hbox.addStretch(1) hbox.addStretch(1)
hbox.addWidget(self.dialog_buttons) hbox.addWidget(close_button)
hbox.addStretch(1) hbox.addStretch(1)
vbox = QtGui.QVBoxLayout() vbox = QtGui.QVBoxLayout()
vbox.addWidget(splitter)
label = QtGui.QLabel('Not yet implemented!')
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
label = QtGui.QLabel('')
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
vbox.addLayout(hbox) vbox.addLayout(hbox)
self.setLayout(vbox) self.setLayout(vbox)
self.show() self.show()
def _pane_switch(self, item):
"""Switch the visible preference pane."""
index = self.list_panes.indexOfTopLevelItem(item)
if index >= 0:
self.stacked_panes.setCurrentIndex(index)
def _save_and_close(self):
for widget in (self.stacked_panes.widget(i)
for i in range(self.stacked_panes.count())):
for key, field in widget.fields.items():
if isinstance(field, QtGui.QComboBox):
text = field.itemText(field.currentIndex())
data = field.itemData(field.currentIndex())
text = data if data else text
elif isinstance(field, QtGui.QCheckBox):
text = "on" if field.isChecked() else "off"
else:
text = field.text()
self.config.set(widget.section_name, key, str(text))
config.write(self.config)
self.parent.apply_preferences()
self.close()
class PreferencesNotificationBlock(QtGui.QVBoxLayout):
"""Display notification settings with drill down to configure."""
def __init__(self, pane, *args):
QtGui.QVBoxLayout.__init__(*(self,) + args)
self.section = "notifications"
self.config = QtGui.QApplication.instance().config
self.pane = pane
self.stack = QtGui.QStackedWidget()
self.table = QtGui.QTableWidget()
fg_color = self.table.palette().text().color().name()
self.action_labels = {
"sound": "Play a sound",
"message": "Show a message in a popup",
"file": "Log to a file",
"taskbar": "Mark taskbar entry",
"tray": "Mark systray/indicator",
"command": "Run a command"}
self.action_icons = {
"sound": utils.qicon_from_theme("media-playback-start"),
"message": utils.qicon_from_theme("dialog-information"),
"file": utils.qicon_from_theme("document-export"),
"taskbar": utils.qicon_from_theme("weechat"),
"tray": utils.qicon_tint("ic_hot", fg_color),
"command": utils.qicon_from_theme("system-run")}
self.icon_widget_qss = "padding:0;min-height:10px;min-width:16px;"
self.table.resizeColumnsToContents()
self.table.setColumnCount(2)
self.table.resizeRowsToContents()
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.table.setHorizontalHeaderLabels(["State", "Type"])
self.table.horizontalHeader().setStretchLastSection(True)
self.table.horizontalHeader().setHighlightSections(False)
self.table.verticalHeader().setVisible(False)
self.table.setShowGrid(False)
self.table.itemSelectionChanged.connect(self._table_row_changed)
self.buftypes = {}
for key, value in config.CONFIG_DEFAULT_NOTIFICATION_OPTIONS:
buftype, optkey = key.split(".")
if buftype not in self.buftypes:
self.buftypes[buftype] = {}
self.buftypes[buftype][optkey] = self.config.get(self.section, key)
for buftype, optkey in self.buftypes.items():
self._insert_type(buftype)
self.update_icons()
self.resize_table()
self.addWidget(self.table)
self.addWidget(self.stack)
self.table.selectRow(0)
def _insert_type(self, buftype):
row = self.table.rowCount()
self.table.insertRow(row)
buftype_item = QtGui.QTableWidgetItem(buftype)
buftype_item.setTextAlignment(QtCore.Qt.AlignCenter)
self.table.setItem(row, 0, QtGui.QTableWidgetItem())
self.table.setItem(row, 1, buftype_item)
subgrid = QtGui.QGridLayout()
subgrid.setColumnStretch(2, 1)
subgrid.setSpacing(10)
for key, qicon in self.action_icons.items():
value = self.buftypes[buftype][key]
line = subgrid.rowCount()
label = IconTextLabel(self.action_labels[key], qicon, 16)
checkbox = QtGui.QCheckBox()
span = 1
edit = None
if key in ("message", "taskbar", "tray"):
checkbox.setChecked(value == "on")
span = 2
elif key == "sound":
edit = PreferencesFileEdit(
checkbox=checkbox, caption='Select a sound file',
filter='Audio Files (*.wav *.mp3 *.ogg)')
elif key == "file":
edit = PreferencesFileEdit(checkbox=checkbox, mode="save")
else:
edit = PreferencesFileEdit(checkbox=checkbox)
if edit:
edit.insert(value)
subgrid.addWidget(edit, line, 2)
else:
edit = checkbox
subgrid.addWidget(label, line, 1, 1, span)
subgrid.addWidget(checkbox, line, 0)
self.pane.fields[buftype + "." + key] = edit
subpane = QtGui.QWidget()
subpane.setLayout(subgrid)
subpane.setMaximumHeight(subgrid.totalMinimumSize().height())
self.stack.addWidget(subpane)
def resize_table(self):
"""Fit the table height to contents."""
height = self.table.horizontalHeader().height()
height = height * (self.table.rowCount() + 1)
height += self.table.contentsMargins().top()
height += self.table.contentsMargins().bottom()
self.table.setMaximumHeight(height)
self.table.setMinimumHeight(height)
self.table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
def update_icons(self):
"""Draw the correct icons in the left col."""
for i in range(self.table.rowCount()):
hbox = QtGui.QHBoxLayout()
iconset = QtGui.QWidget()
buftype = self.table.item(i, 1).text()
for key, qicon in self.action_icons.items():
field = self.pane.fields[buftype + "." + key]
if isinstance(field, QtGui.QCheckBox):
val = "on" if field.isChecked() else "off"
else:
val = field.text()
iconbtn = QtGui.QPushButton()
iconbtn.setContentsMargins(0, 0, 0, 0)
iconbtn.setFlat(True)
iconbtn.setFocusPolicy(QtCore.Qt.NoFocus)
if val and val != "off":
iconbtn.setIcon(qicon)
iconbtn.setStyleSheet(self.icon_widget_qss)
iconbtn.setToolTip(key)
iconbtn.clicked.connect(lambda i=i: self.table.selectRow(i))
hbox.addWidget(iconbtn)
iconset.setLayout(hbox)
self.table.setCellWidget(i, 0, iconset)
def _table_row_changed(self):
row = self.table.selectionModel().selectedRows()[0].row()
self.stack.setCurrentIndex(row)
class PreferencesTreeWidget(QtGui.QTreeWidget):
"""Widget with tree list of preferences."""
def __init__(self, header_label, *args):
QtGui.QTreeWidget.__init__(*(self,) + args)
self.setHeaderLabel(header_label)
self.setRootIsDecorated(False)
self.setMaximumWidth(180)
self.setTextElideMode(QtCore.Qt.ElideNone)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setFocusPolicy(QtCore.Qt.NoFocus)
class PreferencesSliderEdit(QtGui.QSlider):
"""Percentage slider."""
def __init__(self, *args):
QtGui.QSlider.__init__(*(self,) + args)
self.setMinimum(0)
self.setMaximum(100)
self.setTickPosition(QtGui.QSlider.TicksBelow)
self.setTickInterval(5)
def insert(self, percent):
self.setValue(int(percent[:-1]))
def text(self):
return str(self.value()) + "%"
class PreferencesColorEdit(QtGui.QPushButton):
"""Simple color square that changes based on the color selected."""
def __init__(self, *args):
QtGui.QPushButton.__init__(*(self,) + args)
self.color = "#000000"
self.clicked.connect(self._color_picker)
# Some of the configured colors use a astrisk prefix.
# Toggle this on right click.
self.star = False
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._color_star)
def insert(self, color):
"""Insert the desired color for the widget."""
if color[:1] == "*":
self.star = True
color = color[1:]
self.setText("*" if self.star else "")
self.color = color
self.setStyleSheet("background-color: " + color)
def text(self):
"""Returns the hex value of the color."""
return ("*" if self.star else "") + self.color
def _color_picker(self):
color = QtGui.QColorDialog.getColor(self.color)
self.insert(color.name())
def _color_star(self):
self.star = not self.star
self.insert(self.text())
class PreferencesFontEdit(QtGui.QWidget):
"""Font entry and selection."""
def __init__(self, *args):
QtGui.QWidget.__init__(*(self,) + args)
layout = QtGui.QHBoxLayout()
self.checkbox = QtGui.QCheckBox()
self.edit = QtGui.QLineEdit()
self.font = ""
self.qfont = None
self.button = QtGui.QPushButton("C&hoose")
self.button.clicked.connect(self._font_picker)
self.checkbox.toggled.connect(
lambda: self._checkbox_toggled(self.checkbox))
layout.addWidget(self.checkbox)
layout.addWidget(self.edit)
layout.addWidget(self.button)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def insert(self, font_str):
"""Insert the font described by the string."""
self.font = font_str
self.edit.insert(font_str)
if font_str:
self.qfont = utils.Font.str_to_qfont(font_str)
self.edit.setFont(self.qfont)
self.checkbox.setChecked(True)
self._checkbox_toggled(self.checkbox)
else:
self.checkbox.setChecked(False)
self.qfont = None
self._checkbox_toggled(self.checkbox)
def text(self):
"""Returns the human readable font string."""
return self.font
def _font_picker(self):
font, ok = QtGui.QFontDialog.getFont(self.qfont)
if ok:
self.insert(utils.Font.qfont_to_str(font))
def _checkbox_toggled(self, button):
if button.isChecked() is False and not self.font == "":
self.insert("")
self.edit.setEnabled(button.isChecked())
self.button.setEnabled(button.isChecked())
class PreferencesFileEdit(QtGui.QWidget):
"""File entry and selection."""
def __init__(self, checkbox=None, caption="Select a file", filter=None,
mode="open", *args):
QtGui.QWidget.__init__(*(self,) + args)
layout = QtGui.QHBoxLayout()
self.caption = caption
self.filter = filter
self.edit = QtGui.QLineEdit()
self.file_str = ""
self.mode = mode
self.button = QtGui.QPushButton("B&rowse")
self.button.clicked.connect(self._file_picker)
if checkbox:
self.checkbox = checkbox
else:
self.checkbox = QtGui.QCheckBox()
layout.addWidget(self.checkbox)
self.checkbox.toggled.connect(
lambda: self._checkbox_toggled(self.checkbox))
layout.addWidget(self.edit)
layout.addWidget(self.button)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def insert(self, file_str):
"""Insert the file."""
self.file_str = file_str
self.edit.insert(file_str)
if file_str:
self.checkbox.setChecked(True)
self._checkbox_toggled(self.checkbox)
else:
self.checkbox.setChecked(False)
self._checkbox_toggled(self.checkbox)
def text(self):
"""Returns the human readable font string."""
return self.file_str
def _file_picker(self):
path = ""
if self.mode == "save":
fn = QtGui.QFileDialog.getSaveFileName
else:
fn = QtGui.QFileDialog.getOpenFileName
filename, fil = fn(self, self.caption, path, self.filter, self.filter)
if filename:
self.insert(filename)
def _checkbox_toggled(self, button):
if button.isChecked() is False and not self.file_str == "":
self.insert("")
self.edit.setEnabled(button.isChecked())
self.button.setEnabled(button.isChecked())
class PreferencesPaneWidget(QtGui.QWidget):
"""
Widget with (from top to bottom):
title, chat + nicklist (optional) + prompt/input.
"""
disabled_fields = ["show_hostnames", "hide_nick_changes",
"hide_join_and_part"]
def __init__(self, section, section_name):
QtGui.QWidget.__init__(self)
self.grid = QtGui.QGridLayout()
self.grid.setAlignment(QtCore.Qt.AlignTop)
self.section = section
self.section_name = section_name
self.fields = {}
self.setLayout(self.grid)
self.grid.setColumnStretch(2, 1)
self.grid.setSpacing(10)
self.int_validator = QtGui.QIntValidator(0, 2147483647, self)
toolbar_icons = [
('ToolButtonFollowStyle', 'Default'),
('ToolButtonIconOnly', 'Icon Only'),
('ToolButtonTextOnly', 'Text Only'),
('ToolButtonTextBesideIcon', 'Text Alongside Icons'),
('ToolButtonTextUnderIcon', 'Text Under Icons')]
tray_options = [
('always', 'Always'),
('unread', 'On Unread Messages'),
('never', 'Never'),
]
list_positions = [
('left', 'Left'),
('right', 'Right'),
]
sort_options = ['A-Z Ranked', 'A-Z', 'Z-A Ranked', 'Z-A']
spellcheck_langs = [(x, x) for x in
InputLineSpell.list_languages()]
spellcheck_langs.insert(0, ('', ''))
focus_opts = ["requested", "always", "never"]
self.comboboxes = {"style": QtGui.QStyleFactory.keys(),
"position": list_positions,
"toolbar_icons": toolbar_icons,
"focus_new_tabs": focus_opts,
"tray_icon": tray_options,
"sort": sort_options,
"spellcheck_dictionary": spellcheck_langs}
def addItem(self, key, value, default):
"""Add a key-value pair."""
line = len(self.fields)
name = key.split(".")[-1:][0].capitalize().replace("_", " ")
label = QtGui.QLabel(name)
start = 0
if self.section == "color":
start = 2 * (line % 2)
line = line // 2
edit = PreferencesColorEdit()
edit.setFixedWidth(edit.sizeHint().height())
edit.insert(value)
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
elif key == "custom_stylesheet":
edit = PreferencesFileEdit(caption='Select QStyleSheet File',
filter='*.qss')
edit.insert(value)
elif name.lower()[-5:] == "sound":
edit = PreferencesFileEdit(
caption='Select a sound file',
filter='Audio Files (*.wav *.mp3 *.ogg)')
edit.insert(value)
elif name.lower()[-4:] == "font":
edit = PreferencesFontEdit()
edit.setFixedWidth(200)
edit.insert(value)
elif key in self.comboboxes.keys():
edit = QtGui.QComboBox()
if len(self.comboboxes[key][0]) == 2:
for keyvalue in self.comboboxes[key]:
edit.addItem(keyvalue[1], keyvalue[0])
# if self.section == "nicks" and key == "position":
# edit.addItem("below", "Below Buffer List")
# edit.addItem("above", "Above Buffer List")
edit.setCurrentIndex(edit.findData(value))
else:
edit.addItems(self.comboboxes[key])
edit.setCurrentIndex(edit.findText(value))
edit.setFixedWidth(200)
elif default in ["on", "off"]:
edit = QtGui.QCheckBox()
edit.setChecked(value == "on")
elif default[-1:] == "%":
edit = PreferencesSliderEdit(QtCore.Qt.Horizontal)
edit.setFixedWidth(200)
edit.insert(value)
else:
edit = QtGui.QLineEdit()
edit.setFixedWidth(200)
edit.insert(value)
if default.isdigit() or key == "port":
edit.setValidator(self.int_validator)
if key == 'password':
edit.setEchoMode(QtGui.QLineEdit.Password)
if key in self.disabled_fields:
edit.setDisabled(True)
self.grid.addWidget(label, line, start + 0)
self.grid.addWidget(edit, line, start + 1)
self.fields[key] = edit
class IconTextLabel(QtGui.QWidget):
"""An icon next to text."""
def __init__(self, text=None, icon=None, extent=None):
QtGui.QWidget.__init__(self)
text_label = QtGui.QLabel(text)
if not extent:
extent = text_label.height()
icon_label = QtGui.QLabel()
pixmap = icon.pixmap(extent, QtGui.QIcon.Normal, QtGui.QIcon.On)
icon_label.setPixmap(pixmap)
label_layout = QtGui.QHBoxLayout()
label_layout.addWidget(icon_label)
label_layout.addWidget(text_label)
label_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.setLayout(label_layout)

555
qweechat/preferences.py.new Normal file
View File

@ -0,0 +1,555 @@
# -*- coding: utf-8 -*-
#
# preferences.py - preferences dialog box
#
# Copyright (C) 2016 Ricky Brent <ricky@rickybrent.com>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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 3 of the License, or
# (at your option) any later version.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
from qweechat.config import (CONFIG_DEFAULT_SECTIONS,
CONFIG_DEFAULT_NOTIFICATION_OPTIONS,
CONFIG_DEFAULT_OPTIONS, write)
import utils
from qtpy import QtCore, QtGui
qicon_from_theme = QtGui.QIcon.fromTheme
class PreferencesDialog(QtGui.QDialog):
"""Preferences dialog."""
custom_sections = {
"look": "Look",
"input": "Input Box",
"nicks": "Nick List",
"buffers": "Buffer List",
"buffer_flags": False,
"notifications": "Notifications",
"color": "Colors",
"relay": "Relay/Connection"
}
def __init__(self, name, parent, *args):
QtGui.QDialog.__init__(*(self,) + args)
self.setModal(True)
self.setWindowTitle(name)
self.parent = parent
self.config = parent.config
self.stacked_panes = QtGui.QStackedWidget()
self.list_panes = PreferencesTreeWidget("Settings")
splitter = QtGui.QSplitter()
splitter.addWidget(self.list_panes)
splitter.addWidget(self.stacked_panes)
# Follow same order as defaults:
section_panes = {}
for section in CONFIG_DEFAULT_SECTIONS:
item = QtGui.QTreeWidgetItem(section)
name = section
item.setText(0, section.title())
if section in self.custom_sections:
if not self.custom_sections[section]:
continue
item.setText(0, self.custom_sections[section])
section_panes[section] = PreferencesPaneWidget(section, name)
self.list_panes.addTopLevelItem(item)
self.stacked_panes.addWidget(section_panes[section])
for setting, default in CONFIG_DEFAULT_OPTIONS:
section_key = setting.split(".")
section = section_key[0]
key = ".".join(section_key[1:])
section_panes[section].addItem(
key, self.config.get(section, key), default)
for key, value in self.config.items("color"):
section_panes["color"].addItem(key, value, False)
notification_field_count = len(section_panes["notifications"].fields)
notification = PreferencesNotificationBlock(
section_panes["notifications"])
section_panes["notifications"].grid.addLayout(
notification, notification_field_count, 0, 1, -1)
self.list_panes.currentItemChanged.connect(self._pane_switch)
self.list_panes.setCurrentItem(self.list_panes.topLevelItem(0))
hbox = QtGui.QHBoxLayout()
self.dialog_buttons = QtGui.QDialogButtonBox()
self.dialog_buttons.setStandardButtons(
QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close)
self.dialog_buttons.accepted.connect(self._save_and_close)
hbox.addStretch(1)
hbox.addWidget(self.dialog_buttons)
hbox.addStretch(1)
vbox = QtGui.QVBoxLayout()
vbox.addWidget(splitter)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.show()
def _pane_switch(self, item):
"""Switch the visible preference pane."""
index = self.list_panes.indexOfTopLevelItem(item)
if index >= 0:
self.stacked_panes.setCurrentIndex(index)
def _save_and_close(self):
for widget in (self.stacked_panes.widget(i)
for i in range(self.stacked_panes.count())):
for key, field in widget.fields.items():
if isinstance(field, QtGui.QComboBox):
text = field.itemText(field.currentIndex())
data = field.itemData(field.currentIndex())
text = data if data else text
elif isinstance(field, QtGui.QCheckBox):
text = "on" if field.isChecked() else "off"
else:
text = field.text()
self.config.set(widget.section_name, key, str(text))
write(self.config)
self.parent.apply_preferences()
self.close()
class PreferencesNotificationBlock(QtGui.QVBoxLayout):
"""Display notification settings with drill down to configure."""
def __init__(self, pane, *args):
QtGui.QVBoxLayout.__init__(*(self,) + args)
self.section = "notifications"
self.config = QtGui.QApplication.instance().config
self.pane = pane
self.stack = QtGui.QStackedWidget()
self.table = QtGui.QTableWidget()
fg_color = self.table.palette().text().color().name()
self.action_labels = {
"sound": "Play a sound",
"message": "Show a message in a popup",
"file": "Log to a file",
"taskbar": "Mark taskbar entry",
"tray": "Mark systray/indicator",
"command": "Run a command"}
self.action_icons = {
"sound": qicon_from_theme("media-playback-start"),
"message": qicon_from_theme("dialog-information"),
"file": qicon_from_theme("document-export"),
"taskbar": qicon_from_theme("weechat"),
"tray": utils.qicon_tint("ic_hot", fg_color),
"command": qicon_from_theme("system-run")}
self.icon_widget_qss = "padding:0;min-height:10px;min-width:16px;"
self.table.resizeColumnsToContents()
self.table.setColumnCount(2)
self.table.resizeRowsToContents()
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.table.setHorizontalHeaderLabels(["State", "Type"])
self.table.horizontalHeader().setStretchLastSection(True)
self.table.horizontalHeader().setHighlightSections(False)
self.table.verticalHeader().setVisible(False)
self.table.setShowGrid(False)
self.table.itemSelectionChanged.connect(self._table_row_changed)
self.buftypes = {}
for key, value in CONFIG_DEFAULT_NOTIFICATION_OPTIONS:
buftype, optkey = key.split(".")
if buftype not in self.buftypes:
self.buftypes[buftype] = {}
self.buftypes[buftype][optkey] = self.config.get(self.section, key)
for buftype, optkey in self.buftypes.items():
self._insert_type(buftype)
self.update_icons()
self.resize_table()
self.addWidget(self.table)
self.addWidget(self.stack)
self.table.selectRow(0)
def _insert_type(self, buftype):
row = self.table.rowCount()
self.table.insertRow(row)
buftype_item = QtGui.QTableWidgetItem(buftype)
buftype_item.setTextAlignment(QtCore.Qt.AlignCenter)
self.table.setItem(row, 0, QtGui.QTableWidgetItem())
self.table.setItem(row, 1, buftype_item)
subgrid = QtGui.QGridLayout()
subgrid.setColumnStretch(2, 1)
subgrid.setSpacing(10)
for key, qicon in self.action_icons.items():
value = self.buftypes[buftype][key]
line = subgrid.rowCount()
label = IconTextLabel(self.action_labels[key], qicon, 16)
checkbox = QtGui.QCheckBox()
span = 1
edit = None
if key in ("message", "taskbar", "tray"):
checkbox.setChecked(value == "on")
span = 2
elif key == "sound":
edit = PreferencesFileEdit(
checkbox=checkbox, caption='Select a sound file',
filter='Audio Files (*.wav *.mp3 *.ogg)')
elif key == "file":
edit = PreferencesFileEdit(checkbox=checkbox, mode="save")
else:
edit = PreferencesFileEdit(checkbox=checkbox)
if edit:
edit.insert(value)
subgrid.addWidget(edit, line, 2)
else:
edit = checkbox
subgrid.addWidget(label, line, 1, 1, span)
subgrid.addWidget(checkbox, line, 0)
self.pane.fields[buftype + "." + key] = edit
subpane = QtGui.QWidget()
subpane.setLayout(subgrid)
subpane.setMaximumHeight(subgrid.totalMinimumSize().height())
self.stack.addWidget(subpane)
def resize_table(self):
"""Fit the table height to contents."""
height = self.table.horizontalHeader().height()
height = height * (self.table.rowCount() + 1)
height += self.table.contentsMargins().top()
height += self.table.contentsMargins().bottom()
self.table.setMaximumHeight(height)
self.table.setMinimumHeight(height)
self.table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
def update_icons(self):
"""Draw the correct icons in the left col."""
for i in range(self.table.rowCount()):
hbox = QtGui.QHBoxLayout()
iconset = QtGui.QWidget()
buftype = self.table.item(i, 1).text()
for key, qicon in self.action_icons.items():
field = self.pane.fields[buftype + "." + key]
if isinstance(field, QtGui.QCheckBox):
val = "on" if field.isChecked() else "off"
else:
val = field.text()
iconbtn = QtGui.QPushButton()
iconbtn.setContentsMargins(0, 0, 0, 0)
iconbtn.setFlat(True)
iconbtn.setFocusPolicy(QtCore.Qt.NoFocus)
if val and val != "off":
iconbtn.setIcon(qicon)
iconbtn.setStyleSheet(self.icon_widget_qss)
iconbtn.setToolTip(key)
iconbtn.clicked.connect(lambda i=i: self.table.selectRow(i))
hbox.addWidget(iconbtn)
iconset.setLayout(hbox)
self.table.setCellWidget(i, 0, iconset)
def _table_row_changed(self):
row = self.table.selectionModel().selectedRows()[0].row()
self.stack.setCurrentIndex(row)
class PreferencesTreeWidget(QtGui.QTreeWidget):
"""Widget with tree list of preferences."""
def __init__(self, header_label, *args):
QtGui.QTreeWidget.__init__(*(self,) + args)
self.setHeaderLabel(header_label)
self.setRootIsDecorated(False)
self.setMaximumWidth(180)
self.setTextElideMode(QtCore.Qt.ElideNone)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setFocusPolicy(QtCore.Qt.NoFocus)
class PreferencesSliderEdit(QtGui.QSlider):
"""Percentage slider."""
def __init__(self, *args):
QtGui.QSlider.__init__(*(self,) + args)
self.setMinimum(0)
self.setMaximum(100)
self.setTickPosition(QtGui.QSlider.TicksBelow)
self.setTickInterval(5)
def insert(self, percent):
self.setValue(int(percent[:-1]))
def text(self):
return str(self.value()) + "%"
class PreferencesColorEdit(QtGui.QPushButton):
"""Simple color square that changes based on the color selected."""
def __init__(self, *args):
QtGui.QPushButton.__init__(*(self,) + args)
self.color = "#000000"
self.clicked.connect(self._color_picker)
# Some of the configured colors use a astrisk prefix.
# Toggle this on right click.
self.star = False
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._color_star)
def insert(self, color):
"""Insert the desired color for the widget."""
if color[:1] == "*":
self.star = True
color = color[1:]
self.setText("*" if self.star else "")
self.color = color
self.setStyleSheet("background-color: " + color)
def text(self):
"""Returns the hex value of the color."""
return ("*" if self.star else "") + self.color
def _color_picker(self):
color = QtGui.QColorDialog.getColor(self.color)
self.insert(color.name())
def _color_star(self):
self.star = not self.star
self.insert(self.text())
class PreferencesFontEdit(QtGui.QWidget):
"""Font entry and selection."""
def __init__(self, *args):
QtGui.QWidget.__init__(*(self,) + args)
layout = QtGui.QHBoxLayout()
self.checkbox = QtGui.QCheckBox()
self.edit = QtGui.QLineEdit()
self.font = ""
self.qfont = None
self.button = QtGui.QPushButton("C&hoose")
self.button.clicked.connect(self._font_picker)
self.checkbox.toggled.connect(
lambda: self._checkbox_toggled(self.checkbox))
layout.addWidget(self.checkbox)
layout.addWidget(self.edit)
layout.addWidget(self.button)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def insert(self, font_str):
"""Insert the font described by the string."""
self.font = font_str
self.edit.insert(font_str)
if font_str:
self.qfont = utils.Font.str_to_qfont(font_str)
self.edit.setFont(self.qfont)
self.checkbox.setChecked(True)
self._checkbox_toggled(self.checkbox)
else:
self.checkbox.setChecked(False)
self.qfont = None
self._checkbox_toggled(self.checkbox)
def text(self):
"""Returns the human readable font string."""
return self.font
def _font_picker(self):
font, ok = QtGui.QFontDialog.getFont(self.qfont)
if ok:
self.insert(utils.Font.qfont_to_str(font))
def _checkbox_toggled(self, button):
if button.isChecked() is False and not self.font == "":
self.insert("")
self.edit.setEnabled(button.isChecked())
self.button.setEnabled(button.isChecked())
class PreferencesFileEdit(QtGui.QWidget):
"""File entry and selection."""
def __init__(self, checkbox=None, caption="Select a file", filter=None,
mode="open", *args):
QtGui.QWidget.__init__(*(self,) + args)
layout = QtGui.QHBoxLayout()
self.caption = caption
self.filter = filter
self.edit = QtGui.QLineEdit()
self.file_str = ""
self.mode = mode
self.button = QtGui.QPushButton("B&rowse")
self.button.clicked.connect(self._file_picker)
if checkbox:
self.checkbox = checkbox
else:
self.checkbox = QtGui.QCheckBox()
layout.addWidget(self.checkbox)
self.checkbox.toggled.connect(
lambda: self._checkbox_toggled(self.checkbox))
layout.addWidget(self.edit)
layout.addWidget(self.button)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def insert(self, file_str):
"""Insert the file."""
self.file_str = file_str
self.edit.insert(file_str)
if file_str:
self.checkbox.setChecked(True)
self._checkbox_toggled(self.checkbox)
else:
self.checkbox.setChecked(False)
self._checkbox_toggled(self.checkbox)
def text(self):
"""Returns the human readable font string."""
return self.file_str
def _file_picker(self):
path = ""
if self.mode == "save":
fn = QtGui.QFileDialog.getSaveFileName
else:
fn = QtGui.QFileDialog.getOpenFileName
filename, fil = fn(self, self.caption, path, self.filter, self.filter)
if filename:
self.insert(filename)
def _checkbox_toggled(self, button):
if button.isChecked() is False and not self.file_str == "":
self.insert("")
self.edit.setEnabled(button.isChecked())
self.button.setEnabled(button.isChecked())
class PreferencesPaneWidget(QtGui.QWidget):
"""
Widget with (from top to bottom):
title, chat + nicklist (optional) + prompt/input.
"""
disabled_fields = ["show_hostnames", "hide_nick_changes",
"hide_join_and_part"]
def __init__(self, section, section_name):
QtGui.QWidget.__init__(self)
self.grid = QtGui.QGridLayout()
self.grid.setAlignment(QtCore.Qt.AlignTop)
self.section = section
self.section_name = section_name
self.fields = {}
self.setLayout(self.grid)
self.grid.setColumnStretch(2, 1)
self.grid.setSpacing(10)
self.int_validator = QtGui.QIntValidator(0, 2147483647, self)
toolbar_icons = [
('ToolButtonFollowStyle', 'Default'),
('ToolButtonIconOnly', 'Icon Only'),
('ToolButtonTextOnly', 'Text Only'),
('ToolButtonTextBesideIcon', 'Text Alongside Icons'),
('ToolButtonTextUnderIcon', 'Text Under Icons')]
tray_options = [
('always', 'Always'),
('unread', 'On Unread Messages'),
('never', 'Never'),
]
list_positions = [
('left', 'Left'),
('right', 'Right'),
]
sort_options = ['A-Z Ranked', 'A-Z', 'Z-A Ranked', 'Z-A']
focus_opts = ["requested", "always", "never"]
self.comboboxes = {"style": QtGui.QStyleFactory.keys(),
"position": list_positions,
"toolbar_icons": toolbar_icons,
"focus_new_tabs": focus_opts,
"tray_icon": tray_options,
"sort": sort_options}
def addItem(self, key, value, default):
"""Add a key-value pair."""
line = len(self.fields)
name = key.split(".")[-1:][0].capitalize().replace("_", " ")
label = QtGui.QLabel(name)
start = 0
if self.section == "color":
start = 2 * (line % 2)
line = line // 2
edit = PreferencesColorEdit()
edit.setFixedWidth(edit.sizeHint().height())
edit.insert(value)
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
elif key == "custom_stylesheet":
edit = PreferencesFileEdit(caption='Select QStyleSheet File',
filter='*.qss')
edit.insert(value)
elif name.lower()[-5:] == "sound":
edit = PreferencesFileEdit(
caption='Select a sound file',
filter='Audio Files (*.wav *.mp3 *.ogg)')
edit.insert(value)
elif name.lower()[-4:] == "font":
edit = PreferencesFontEdit()
edit.setFixedWidth(200)
edit.insert(value)
elif key in self.comboboxes.keys():
edit = QtGui.QComboBox()
if len(self.comboboxes[key][0]) == 2:
for keyvalue in self.comboboxes[key]:
edit.addItem(keyvalue[1], keyvalue[0])
# if self.section == "nicks" and key == "position":
# edit.addItem("below", "Below Buffer List")
# edit.addItem("above", "Above Buffer List")
edit.setCurrentIndex(edit.findData(value))
else:
edit.addItems(self.comboboxes[key])
edit.setCurrentIndex(edit.findText(value))
edit.setFixedWidth(200)
elif default in ["on", "off"]:
edit = QtGui.QCheckBox()
edit.setChecked(value == "on")
elif default[-1:] == "%":
edit = PreferencesSliderEdit(QtCore.Qt.Horizontal)
edit.setFixedWidth(200)
edit.insert(value)
else:
edit = QtGui.QLineEdit()
edit.setFixedWidth(200)
edit.insert(value)
if default.isdigit() or key == "port":
edit.setValidator(self.int_validator)
if key == 'password':
edit.setEchoMode(QtGui.QLineEdit.Password)
if key in self.disabled_fields:
edit.setDisabled(True)
self.grid.addWidget(label, line, start + 0)
self.grid.addWidget(edit, line, start + 1)
self.fields[key] = edit
class IconTextLabel(QtGui.QWidget):
"""An icon next to text."""
def __init__(self, text=None, icon=None, extent=None):
QtGui.QWidget.__init__(self)
text_label = QtGui.QLabel(text)
if not extent:
extent = text_label.height()
icon_label = QtGui.QLabel()
pixmap = icon.pixmap(extent, QtGui.QIcon.Normal, QtGui.QIcon.On)
icon_label.setPixmap(pixmap)
label_layout = QtGui.QHBoxLayout()
label_layout.addWidget(icon_label)
label_layout.addWidget(text_label)
label_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.setLayout(label_layout)

View File

@ -39,7 +39,7 @@ from pkg_resources import resource_filename
from qtpy import QtCore, QtGui, QtWidgets from qtpy import QtCore, QtGui, QtWidgets
from qweechat import config from qweechat.config import read, write
from qweechat.about import AboutDialog from qweechat.about import AboutDialog
from qweechat.buffer import BufferListWidget, Buffer from qweechat.buffer import BufferListWidget, Buffer
from qweechat.connection import ConnectionDialog from qweechat.connection import ConnectionDialog
@ -59,7 +59,7 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args): def __init__(self, *args):
super().__init__(*args) super().__init__(*args)
self.config = config.read() self.config = read()
self.resize(1000, 600) self.resize(1000, 600)
self.setWindowTitle(APP_NAME) self.setWindowTitle(APP_NAME)
@ -536,7 +536,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.network.disconnect_weechat() self.network.disconnect_weechat()
if self.network.debug_dialog: if self.network.debug_dialog:
self.network.debug_dialog.close() self.network.debug_dialog.close()
config.write(self.config) write(self.config)
QtWidgets.QFrame.closeEvent(self, event) QtWidgets.QFrame.closeEvent(self, event)

View File

@ -22,7 +22,7 @@
"""Version of QWeeChat.""" """Version of QWeeChat."""
VERSION = '0.0.1-dev' VERSION = '0.0.1'
def qweechat_version(): def qweechat_version():

View File

@ -1,3 +1,3 @@
# works with PyQt5 and maybe PySide as well # works with PyQt5 and maybe PySide as well
PyQt6 # PyQt5
# dev-python/qtconsole >= 5.5.1

View File

@ -56,6 +56,7 @@ setup(
] ]
}, },
install_requires=[ install_requires=[
'PyQt6', 'PyQt5',
], ],
zip_safe = False,
) )