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
# 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
check: lint test
sh ${PYTHON_EXE_MSYS} -c "import ${MOD}"
lint: flake8 pylint bandit
lint:: flake8 pylint bandit
flake8:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
@ -32,3 +42,8 @@ pylint:
bandit:
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 qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import pyqtSignal as Signal
from qtpy.QtCore import Signal
from qweechat.chat import ChatTextEdit
from qweechat.input import InputLineEdit

View file

@ -26,7 +26,7 @@ import datetime
from qtpy import QtCore, QtWidgets, QtGui
from qweechat import config
from qweechat.config import color_options
from qweechat.weechat import color
@ -62,7 +62,7 @@ class ChatTextEdit(QtWidgets.QTextEdit):
'/': 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):
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)),
('look.debug', 'off'),
('look.statusbar', 'on'))
CONFIG_DEFAULT_NOTIFICATION_OPTIONS = tuple
# Default colors for WeeChat color options (option name, #rgb value)
CONFIG_DEFAULT_COLOR_OPTIONS = (

View file

@ -232,7 +232,7 @@ class Network(QtCore.QObject):
def is_connected(self):
"""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'):
"""Return True if the socket is connected, False otherwise."""

View file

@ -2,7 +2,7 @@
#
# 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.
#
@ -20,541 +20,38 @@
# along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
import qt_compat
import config
import utils
from inputlinespell import InputLineSpell
"""Preferences dialog box."""
QtCore = qt_compat.import_module('QtCore')
QtGui = qt_compat.import_module('QtGui')
from PyQt5 import QtCore, QtWidgets as QtGui
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):
def __init__(self, *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")
self.setWindowTitle('Preferences')
splitter = QtGui.QSplitter()
splitter.addWidget(self.list_panes)
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))
close_button = QtGui.QPushButton('Close')
close_button.pressed.connect(self.close)
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.addWidget(close_button)
hbox.addStretch(1)
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)
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))
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 qweechat import config
from qweechat.config import read, write
from qweechat.about import AboutDialog
from qweechat.buffer import BufferListWidget, Buffer
from qweechat.connection import ConnectionDialog
@ -59,7 +59,7 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self.config = config.read()
self.config = read()
self.resize(1000, 600)
self.setWindowTitle(APP_NAME)
@ -536,7 +536,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.network.disconnect_weechat()
if self.network.debug_dialog:
self.network.debug_dialog.close()
config.write(self.config)
write(self.config)
QtWidgets.QFrame.closeEvent(self, event)

View file

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

View file

@ -1,3 +1,3 @@
# 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=[
'PyQt6',
'PyQt5',
],
zip_safe = False,
)