1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "actiondialog.h"
21 #include "ui_actiondialog.h"
22 
23 #include "common/appconfig.h"
24 #include "common/command.h"
25 #include "common/config.h"
26 #include "common/mimetypes.h"
27 #include "common/textdata.h"
28 #include "item/serialize.h"
29 
30 #include <QAbstractButton>
31 #include <QFile>
32 #include <QMessageBox>
33 #include <QShortcut>
34 
35 #include <memory>
36 
37 namespace {
38 
initFormatComboBox(QComboBox * combo,const QStringList & additionalFormats=QStringList ())39 void initFormatComboBox(QComboBox *combo, const QStringList &additionalFormats = QStringList())
40 {
41     QStringList formats = QStringList() << QString() << QString(mimeText) << additionalFormats;
42     formats.removeDuplicates();
43     combo->clear();
44     combo->addItems(formats);
45 }
46 
wasChangedByUser(QObject * object)47 bool wasChangedByUser(QObject *object)
48 {
49     return object->property("UserChanged").toBool();
50 }
51 
setChangedByUser(QWidget * object)52 void setChangedByUser(QWidget *object)
53 {
54     object->setProperty("UserChanged", object->hasFocus());
55 }
56 
commandToLabel(const QString & command)57 QString commandToLabel(const QString &command)
58 {
59     QString label = command.size() > 48 ? command.left(48) + "..." : command;
60     label.replace('\n', " ");
61     label.replace(QRegularExpression("\\s\\+"), " ");
62     return label;
63 }
64 
65 } // namespace
66 
ActionDialog(QWidget * parent)67 ActionDialog::ActionDialog(QWidget *parent)
68     : QDialog(parent)
69     , ui(new Ui::ActionDialog)
70     , m_data()
71     , m_currentCommandIndex(-1)
72 {
73     ui->setupUi(this);
74 
75     auto shortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_P), this);
76     connect(shortcut, &QShortcut::activated, this, &ActionDialog::previousCommand);
77     shortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_N), this);
78     connect(shortcut, &QShortcut::activated, this, &ActionDialog::nextCommand);
79 
80     connect(ui->buttonBox, &QDialogButtonBox::clicked,
81             this, &ActionDialog::onButtonBoxClicked);
82     connect(ui->comboBoxCommands, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
83             this, &ActionDialog::onComboBoxCommandsCurrentIndexChanged);
84     connect(ui->comboBoxInputFormat, &QComboBox::currentTextChanged,
85             this, &ActionDialog::onComboBoxInputFormatCurrentTextChanged);
86     connect(ui->comboBoxOutputFormat, &QComboBox::editTextChanged,
87             this, &ActionDialog::onComboBoxOutputFormatEditTextchanged);
88     connect(ui->comboBoxOutputTab, &QComboBox::editTextChanged,
89             this, &ActionDialog::onComboBoxOutputTabEditTextChanged);
90     connect(ui->separatorEdit, &QLineEdit::textEdited,
91             this, &ActionDialog::onSeparatorEditTextEdited);
92 
93     onComboBoxInputFormatCurrentTextChanged(QString());
94     onComboBoxOutputFormatEditTextchanged(QString());
95     loadSettings();
96 }
97 
~ActionDialog()98 ActionDialog::~ActionDialog()
99 {
100     delete ui;
101 }
102 
setInputData(const QVariantMap & data)103 void ActionDialog::setInputData(const QVariantMap &data)
104 {
105     m_data = data;
106 
107     QString defaultFormat = ui->comboBoxInputFormat->currentText();
108     initFormatComboBox(ui->comboBoxInputFormat, data.keys());
109     const int index = qMax(0, ui->comboBoxInputFormat->findText(defaultFormat));
110     ui->comboBoxInputFormat->setCurrentIndex(index);
111 }
112 
restoreHistory()113 void ActionDialog::restoreHistory()
114 {
115     const int maxCount = AppConfig().option<Config::command_history_size>();
116     ui->comboBoxCommands->setMaxCount(maxCount + 1);
117 
118     QFile file( dataFilename() );
119     file.open(QIODevice::ReadOnly);
120     QDataStream in(&file);
121     QVariant v;
122 
123     ui->comboBoxCommands->clear();
124     ui->comboBoxCommands->addItem(QString());
125     while( !in.atEnd() && ui->comboBoxCommands->count() <= maxCount ) {
126         in >> v;
127         const QVariantMap values = v.value<QVariantMap>();
128         const QString cmd = values.value("cmd").toString();
129         ui->comboBoxCommands->addItem( commandToLabel(cmd), v );
130     }
131     ui->comboBoxCommands->setCurrentIndex(0);
132 }
133 
dataFilename() const134 const QString ActionDialog::dataFilename() const
135 {
136     return getConfigurationFilePath("_cmds.dat");
137 }
138 
saveHistory()139 void ActionDialog::saveHistory()
140 {
141     QFile file( dataFilename() );
142     file.open(QIODevice::WriteOnly);
143     QDataStream out(&file);
144 
145     for (int i = 1; i < ui->comboBoxCommands->count(); ++i) {
146         const QVariant itemData = ui->comboBoxCommands->itemData(i);
147         out << itemData;
148     }
149 }
150 
setCommand(const Command & cmd)151 void ActionDialog::setCommand(const Command &cmd)
152 {
153     ui->comboBoxCommands->setCurrentIndex(0);
154     ui->commandEdit->setCommand(cmd.cmd);
155     ui->separatorEdit->setText(cmd.sep);
156 
157     int index = ui->comboBoxInputFormat->findText(cmd.input);
158     if (index == -1) {
159         ui->comboBoxInputFormat->insertItem(0, cmd.input);
160         index = 0;
161     }
162     ui->comboBoxInputFormat->setCurrentIndex(index);
163 
164     ui->comboBoxOutputFormat->setEditText(cmd.output);
165 }
166 
setOutputTabs(const QStringList & tabs)167 void ActionDialog::setOutputTabs(const QStringList &tabs)
168 {
169     QComboBox *w = ui->comboBoxOutputTab;
170     w->clear();
171     w->addItem("");
172     w->addItems(tabs);
173 }
174 
setCurrentTab(const QString & currentTabName)175 void ActionDialog::setCurrentTab(const QString &currentTabName)
176 {
177     ui->comboBoxOutputTab->setEditText(currentTabName);
178 }
179 
loadSettings()180 void ActionDialog::loadSettings()
181 {
182     initFormatComboBox(ui->comboBoxInputFormat);
183     initFormatComboBox(ui->comboBoxOutputFormat);
184     restoreHistory();
185 }
186 
command() const187 Command ActionDialog::command() const
188 {
189     Command cmd;
190 
191     cmd.cmd = ui->commandEdit->command();
192     cmd.name = commandToLabel(cmd.cmd);
193     cmd.input = ui->comboBoxInputFormat->currentText();
194     cmd.output = ui->comboBoxOutputFormat->currentText();
195     cmd.sep = ui->separatorEdit->text();
196     cmd.outputTab = ui->comboBoxOutputTab->currentText();
197 
198     return cmd;
199 }
200 
onButtonBoxClicked(QAbstractButton * button)201 void ActionDialog::onButtonBoxClicked(QAbstractButton* button)
202 {
203     switch ( ui->buttonBox->standardButton(button) ) {
204     case QDialogButtonBox::Ok:
205         acceptCommand();
206         saveCurrentCommandToHistory();
207         close();
208         break;
209 
210     case QDialogButtonBox::Apply:
211         acceptCommand();
212         saveCurrentCommandToHistory();
213         break;
214 
215     case QDialogButtonBox::Save:
216         emit saveCommand(command());
217         QMessageBox::information(
218                     this, tr("Command saved"),
219                     tr("Command was saved and can be accessed from item menu.\n"
220                        "You can set up the command in preferences.") );
221         break;
222 
223     case QDialogButtonBox::Cancel:
224         close();
225         break;
226 
227     default:
228         break;
229     }
230 }
231 
onComboBoxCommandsCurrentIndexChanged(int index)232 void ActionDialog::onComboBoxCommandsCurrentIndexChanged(int index)
233 {
234     if ( m_currentCommandIndex >= 0 && m_currentCommandIndex < ui->comboBoxCommands->count() ) {
235         QVariant itemData = createCurrentItemData();
236         if (itemData != ui->comboBoxCommands->itemData(m_currentCommandIndex))
237             ui->comboBoxCommands->setItemData(m_currentCommandIndex, itemData);
238     }
239 
240     m_currentCommandIndex = index;
241 
242     // Restore values from history.
243     QVariant v = ui->comboBoxCommands->itemData(index);
244     QVariantMap values = v.value<QVariantMap>();
245 
246     ui->commandEdit->setCommand(values.value("cmd").toString());
247 
248     // Don't automatically change values if they were edited by user.
249     if ( !wasChangedByUser(ui->comboBoxInputFormat) ) {
250         int i = ui->comboBoxInputFormat->findText(values.value("input").toString());
251         if (i != -1)
252             ui->comboBoxInputFormat->setCurrentIndex(i);
253     }
254 
255     if ( !wasChangedByUser(ui->comboBoxOutputFormat) )
256         ui->comboBoxOutputFormat->setEditText(values.value("output").toString());
257 
258     if ( !wasChangedByUser(ui->separatorEdit) )
259         ui->separatorEdit->setText(values.value("sep").toString());
260 
261     const auto outputTab = values.value("outputTab").toString();
262     if ( !wasChangedByUser(ui->comboBoxOutputTab) && !outputTab.isEmpty() )
263         ui->comboBoxOutputTab->setEditText(values.value(outputTab).toString());
264 }
265 
onComboBoxInputFormatCurrentTextChanged(const QString & format)266 void ActionDialog::onComboBoxInputFormatCurrentTextChanged(const QString &format)
267 {
268     setChangedByUser(ui->comboBoxInputFormat);
269 
270     const bool show = format.startsWith("text", Qt::CaseInsensitive);
271     ui->inputText->setVisible(show);
272 
273     QString text;
274     if ((show || format.isEmpty()) && !m_data.isEmpty() )
275         text = getTextData( m_data, format.isEmpty() ? mimeText : format );
276     ui->inputText->setPlainText(text);
277 }
278 
onComboBoxOutputFormatEditTextchanged(const QString & text)279 void ActionDialog::onComboBoxOutputFormatEditTextchanged(const QString &text)
280 {
281     setChangedByUser(ui->comboBoxOutputFormat);
282 
283     const bool showSeparator = text.startsWith("text", Qt::CaseInsensitive);
284     ui->separatorLabel->setVisible(showSeparator);
285     ui->separatorEdit->setVisible(showSeparator);
286 
287     const bool showOutputTab = !text.isEmpty();
288     ui->labelOutputTab->setVisible(showOutputTab);
289     ui->comboBoxOutputTab->setVisible(showOutputTab);
290 }
291 
onComboBoxOutputTabEditTextChanged(const QString &)292 void ActionDialog::onComboBoxOutputTabEditTextChanged(const QString &)
293 {
294     setChangedByUser(ui->comboBoxOutputTab);
295 }
296 
onSeparatorEditTextEdited(const QString &)297 void ActionDialog::onSeparatorEditTextEdited(const QString &)
298 {
299     setChangedByUser(ui->separatorEdit);
300 }
301 
nextCommand()302 void ActionDialog::nextCommand()
303 {
304     const int index = ui->comboBoxCommands->currentIndex();
305     ui->comboBoxCommands->setCurrentIndex(index - 1);
306 }
307 
previousCommand()308 void ActionDialog::previousCommand()
309 {
310     const int index = ui->comboBoxCommands->currentIndex();
311     ui->comboBoxCommands->setCurrentIndex(index + 1);
312 }
313 
acceptCommand()314 void ActionDialog::acceptCommand()
315 {
316     const auto command = this->command();
317 
318     auto re = command.re;
319     const QString text = getTextData(m_data);
320     const auto m = re.match(text);
321     auto capturedTexts = m.capturedTexts();
322     if ( capturedTexts.isEmpty() )
323         capturedTexts.append(QString());
324     capturedTexts[0] = text;
325 
326     if ( ui->inputText->isVisible() )
327         m_data[mimeText] = ui->inputText->toPlainText();
328 
329     emit commandAccepted(command, capturedTexts, m_data);
330 }
331 
createCurrentItemData()332 QVariant ActionDialog::createCurrentItemData()
333 {
334     QVariantMap values;
335     values["cmd"] = ui->commandEdit->command();
336     values["input"] = ui->comboBoxInputFormat->currentText();
337     values["output"] = ui->comboBoxOutputFormat->currentText();
338     values["sep"] = ui->separatorEdit->text();
339     values["outputTab"] = ui->comboBoxOutputTab->currentText();
340 
341     return values;
342 }
343 
saveCurrentCommandToHistory()344 void ActionDialog::saveCurrentCommandToHistory()
345 {
346     const QString cmd = ui->commandEdit->command();
347     const QVariant itemData = createCurrentItemData();
348     ui->comboBoxCommands->setCurrentIndex(0);
349 
350     for (int i = ui->comboBoxCommands->count() - 1; i >= 1; --i) {
351         const QVariant itemData2 = ui->comboBoxCommands->itemData(i);
352         const QString cmd2 = itemData2.toMap().value("cmd").toString();
353         if (cmd2.isEmpty() || cmd == cmd2)
354             ui->comboBoxCommands->removeItem(i);
355     }
356 
357     if ( cmd.isEmpty() ) {
358         ui->comboBoxCommands->setItemData(0, itemData);
359     } else {
360         ui->comboBoxCommands->insertItem(1, commandToLabel(cmd), itemData);
361         ui->comboBoxCommands->setCurrentIndex(1);
362     }
363 
364     saveHistory();
365 }
366