diff --git a/qweechat/preferences.py b/qweechat/preferences.py index 64315cf..97feece 100644 --- a/qweechat/preferences.py +++ b/qweechat/preferences.py @@ -2,7 +2,7 @@ # # preferences.py - preferences dialog box # -# Copyright (C) 2011-2022 Sébastien Helleu +# Copyright (C) 2016 Ricky Brent # # This file is part of QWeeChat, a Qt remote GUI for WeeChat. # @@ -20,38 +20,541 @@ # along with QWeeChat. If not, see . # -"""Preferences dialog box.""" +import qt_compat +import config +import utils +from inputlinespell import InputLineSpell -from qtpy import QtCore, QtWidgets as QtGui +QtCore = qt_compat.import_module('QtCore') +QtGui = qt_compat.import_module('QtGui') class PreferencesDialog(QtGui.QDialog): """Preferences dialog.""" - def __init__(self, *args): + 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('Preferences') + self.setWindowTitle(name) + self.parent = parent + self.config = parent.config + self.stacked_panes = QtGui.QStackedWidget() + self.list_panes = PreferencesTreeWidget("Settings") - close_button = QtGui.QPushButton('Close') - close_button.pressed.connect(self.close) + 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)) 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(close_button) + hbox.addWidget(self.dialog_buttons) hbox.addStretch(1) vbox = QtGui.QVBoxLayout() - - 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.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)) + 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) diff --git a/qweechat/qweechat.py b/qweechat/qweechat.py index 7bf059f..ed6187a 100644 --- a/qweechat/qweechat.py +++ b/qweechat/qweechat.py @@ -196,8 +196,8 @@ class MainWindow(QtWidgets.QMainWindow): # auto-connect to relay if self.config.getboolean('relay', 'autoconnect'): self.network.connect_weechat( - hostname=self.config.get('relay', 'hostname', fallback=''), - port=self.config.get('relay', 'port', fallback=''), + hostname=self.config.get('relay', 'hostname', fallback='127.0.0.1'), + port=self.config.get('relay', 'port', fallback='9000'), ssl=self.config.getboolean('relay', 'ssl', fallback=''), password=self.config.get('relay', 'password', fallback=''), totp=None,