frontend/mac/resultset/MResultsetViewer.mm (418 lines of code) (raw):
/*
* Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms, as
* designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
* This program 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, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#import "MResultsetViewer.h"
#include "sqlide/recordset_be.h"
#import "MQResultSetCell.h"
#import "MQIndicatorCell.h"
#import "GRTIconCache.h"
#import "MCPPUtilities.h"
#import "MVerticalLayoutView.h"
#include "mforms/toolbar.h"
static NSImage *ascendingSortIndicator= nil;
static NSImage *descendingSortIndicator= nil;
@interface MResultsetViewer()
{
NSMutableArray *nibObjects;
NSFont *mFont;
std::list<boost::signals2::connection> mSigConns;
std::shared_ptr<Recordset> *mData;
int mWarnedManyColumns;
BOOL mPendingRefresh;
}
@end
@implementation MResultsetViewer
@synthesize view;
@synthesize gridView;
+ (void)initialize
{
ascendingSortIndicator = [NSImage imageNamed:@"NSAscendingSortIndicator"];
descendingSortIndicator = [NSImage imageNamed:@"NSDescendingSortIndicator"];
}
- (instancetype)init
{
return [self initWithRecordset: std::shared_ptr<Recordset>()];
}
- (instancetype)initWithRecordset: (Recordset::Ref)rset
{
if (!rset)
return nil;
self = [super init];
if (self != nil)
{
NSBundle *bundle = [NSBundle bundleForClass: self.class];
NSMutableArray *temp;
BOOL loaded = [bundle loadNibNamed: @"WbResultsetView" owner: self topLevelObjects: &temp];
if (loaded)
{
nibObjects = temp;
mData = new Recordset::Ref();
*mData = rset;
[gridView setRecordset: mData->get()];
(*mData)->update_edited_field = std::bind(selected_record_changed, (__bridge void *)self);
(*mData)->tree_changed_signal()->connect(std::bind(onRefreshWhenIdle, (__bridge void *)self));
(*mData)->refresh_ui_signal.connect(std::bind(onRefresh, (__bridge void *)self));
(*mData)->rows_changed = std::bind(onRefresh, (__bridge void *)self);
gridView.intercellSpacing = NSMakeSize(0, 1);
gridView.actionDelegate = self;
gridView.allowsMultipleSelection = YES;
(gridView.enclosingScrollView).borderType = NSNoBorder;
mforms::ToolBar *tbar = (*mData)->get_toolbar();
if (tbar->find_item("record_edit"))
{
tbar->find_item("record_edit")->signal_activated()->connect(std::bind(record_edit, (__bridge void *)self));
tbar->find_item("record_add")->signal_activated()->connect(std::bind(record_add, (__bridge void *)self));
tbar->find_item("record_del")->signal_activated()->connect(std::bind(record_del, (__bridge void *)self));
}
[self rebuildColumns];
}
}
return self;
}
- (void)dealloc
{
[NSObject cancelPreviousPerformRequestsWithTarget: self];
(*mData)->refresh_ui_signal.disconnect_all_slots();
std::for_each(mSigConns.begin(), mSigConns.end(), std::bind(&boost::signals2::connection::disconnect, std::placeholders::_1));
delete mData;
}
// for use by mforms
static const char *viewFlagsKey = "viewFlagsKey";
- (NSInteger)viewFlags
{
NSNumber *value = objc_getAssociatedObject(self, viewFlagsKey);
return value.intValue;
}
- (void)setViewFlags: (NSInteger)value
{
objc_setAssociatedObject(self, viewFlagsKey, @(value), OBJC_ASSOCIATION_RETAIN);
}
//
- (void)setHeaderIndicator:(int)indicator forColumn:(int)column
{
NSTableColumn *tableColumn= [gridView tableColumnWithIdentifier:[NSString stringWithFormat:@"%i", column]];
switch (indicator)
{
case 0:
[gridView setIndicatorImage: nil inTableColumn:tableColumn];
break;
case 1:
[gridView setIndicatorImage: ascendingSortIndicator inTableColumn:tableColumn];
break;
case -1:
[gridView setIndicatorImage: descendingSortIndicator inTableColumn:tableColumn];
break;
}
}
- (void)rebuildColumns
{
for (NSUInteger i = gridView.tableColumns.count - 1; i > 0; --i) {
[gridView removeTableColumn: gridView.tableColumns[i]];
}
if (mWarnedManyColumns == 0 && (*mData)->get_column_count() > 300)
{
NSAlert *alert = [NSAlert new];
alert.messageText = @"Too Many Columns";
alert.informativeText = @"The resultset for your query contains too many columns, which may be very slow to display."
"\nHowever, as a workaround, manual resizing of columns can be disabled to speed up display and scrolling.";
alert.alertStyle = NSAlertStyleWarning;
[alert addButtonWithTitle: @"Disable Column Resizing"];
[alert addButtonWithTitle: @"Ignore"];
if ([alert runModal] == NSAlertFirstButtonReturn)
mWarnedManyColumns = 1;
else
mWarnedManyColumns = -1;
}
float rowHeight = 0;
for (size_t index = 0, count = (*mData)->get_column_count(); index < count; ++index)
{
std::string label= base::sanitize_utf8((*mData)->get_column_caption(index));
NSTableColumn *column= [[NSTableColumn alloc] initWithIdentifier: [NSString stringWithFormat: @"%zu", index]];
[column.headerCell setTitle: @(label.c_str())];
[column setEditable: YES];
column.dataCell = [[MQResultSetCell alloc] init];
[column.dataCell setEditable: YES];
[column.dataCell setLineBreakMode: NSLineBreakByTruncatingTail];
if (mFont)
{
[column.dataCell setFont: mFont];
rowHeight = MAX(rowHeight, [[column dataCell] cellSize].height + 1);
}
if (mWarnedManyColumns == 1)
column.resizingMask = 0;
[gridView addTableColumn: column];
}
if (rowHeight > 0)
gridView.rowHeight = rowHeight;
}
- (void)setFont:(NSFont*)font
{
mFont = font;
float rowHeight = 0;
for (NSTableColumn *column in gridView.tableColumns)
{
if (mFont)
{
[column.dataCell setFont: mFont];
rowHeight = MAX(rowHeight, [[column dataCell] cellSize].height + 1);
}
}
if (rowHeight > 0)
gridView.rowHeight = rowHeight;
}
- (void)refreshGrid
{
mPendingRefresh = NO;
[gridView reloadData];
}
static int onRefreshWhenIdle(void *viewer_)
{
MResultsetViewer *viewer = (__bridge MResultsetViewer *)viewer_;
// Do table refresh only if it isn't currently in edit mode or this will
// stop any ongoing edit action (and has other side effects like a misplaced selection).
if (!viewer->mPendingRefresh && viewer.gridView.editedRow == -1)
{
viewer->mPendingRefresh = YES;
[viewer performSelector: @selector(refreshGrid) withObject:nil afterDelay: 0];
}
return 0;
}
- (void)clickedTable:(id)sender
{
(*self->mData)->set_edited_field(gridView.clickedRow, gridView.clickedColumn);
[gridView editColumn: gridView.clickedColumn
row: gridView.clickedRow
withEvent: NSApp.currentEvent
select: YES];
}
- (BOOL)hasPendingChanges
{
int upd_count= 0, ins_count= 0, del_count= 0;
(*self->mData)->pending_changes(upd_count, ins_count, del_count);
return upd_count>0 || ins_count>0 || del_count>0;
}
static void record_edit(void *view)
{
MResultsetViewer *viewer = (__bridge MResultsetViewer *)view;
[viewer.gridView editColumn: viewer.gridView.selectedColumnIndex
row: viewer.gridView.selectedRowIndex
withEvent: nil
select: NO];
}
static void record_add(void *view)
{
MResultsetViewer *viewer = (__bridge MResultsetViewer *)view;
[viewer.gridView scrollRowToVisible: (*viewer->mData)->count() - 1];
[viewer.gridView selectCellAtRow: (int)(*viewer->mData)->count() - 1 column: 1];
[viewer.gridView editColumn: viewer.gridView.selectedColumnIndex
row: viewer.gridView.selectedRowIndex
withEvent: nil
select: NO];
}
static void record_del(void *view)
{
MResultsetViewer *viewer = (__bridge MResultsetViewer *)view;
[viewer.gridView deleteSelectedRows];
}
static void selected_record_changed(void *theViewer)
{
MResultsetViewer *viewer = (__bridge MResultsetViewer *)theViewer;
[viewer.gridView scrollRowToVisible: (*viewer->mData)->edited_field_row()-1];
[viewer.gridView deselectAll: nil];
[viewer.gridView selectCellAtRow: (int)(*viewer->mData)->edited_field_row() column: (int)(*viewer->mData)->edited_field_column()];
}
- (void)actionTriggered
{
(*self->mData)->set_edited_field(gridView.selectedRowIndex, gridView.selectedColumnIndex - 1) ;
}
- (std::shared_ptr<Recordset>)recordset
{
return *mData;
}
- (void)activateToolbarItem:(id)sender
{
// TODO: leftover toolbar item handlers, these should be added back to the toolbar maybe.
{
std::string action = [[sender cell].representedObject UTF8String];
int selectedColumnIndex = gridView.selectedColumnIndex;
int selectedRowIndex = gridView.selectedRowIndex;
std::vector<int> rows;
rows.push_back(selectedRowIndex);
if (!(*mData)->action_list().trigger_action(action, rows, selectedColumnIndex)
&& !(*mData)->action_list().trigger_action(action))
{
if (action == "record_first")
{
[gridView scrollRowToVisible: 0];
[gridView selectCellAtRow: 0 column: 1];
}
else if (action == "record_back")
{
int row = gridView.selectedRowIndex - 1;
if (row < 0)
row = 0;
[gridView scrollRowToVisible: row];
[gridView selectCellAtRow: row column: 1];
}
else if (action == "record_next")
{
size_t row = gridView.selectedRowIndex + 1;
if (row >= (*mData)->count() - 1)
row = (*mData)->count() - 1;
[gridView scrollRowToVisible: row];
[gridView selectCellAtRow: (int)row column: 1];
}
else if (action == "record_last")
{
[gridView scrollRowToVisible: (*mData)->count()-1];
[gridView selectCellAtRow: (int)(*mData)->count() - 1 column: 1];
}
else if (action == "record_wrap_vertical")
{
}
else if (action == "record_sort_asc")
{
int column = gridView.selectedColumnIndex - 1;
if (column >= 0)
{
(*mData)->sort_by(column, 1, false);
}
}
else if (action == "record_sort_desc")
{
int column = gridView.selectedColumnIndex - 1;
if (column >= 0)
{
(*mData)->sort_by(column, -1, false);
}
}
else
NSLog(@"unhandled toolbar action %s", action.c_str());
}
}
}
- (void)refresh
{
[gridView reloadData];
}
- (void)refreshFull
{
(*mData)->refresh();
}
static int onRefresh(void *viewer)
{
[(__bridge id)viewer refresh];
return 0;
}
- (void)fixupLayout
{
NSRect rect = gridView.enclosingScrollView.frame;
if (NSHeight(rect) + 20 != NSHeight(view.frame))
{
rect.size.height= NSHeight(view.frame) - 20;
gridView.enclosingScrollView.frame = rect;
}
}
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if (mData && aTableColumn.identifier != nil)
{
int columnIndex = aTableColumn.identifier.intValue;
if ((*mData)->get_column_type(columnIndex) != bec::GridModel::BlobType)
{
std::string text;
(*mData)->get_field_repr(rowIndex, columnIndex, text);
text = base::replaceString(text, "\n", " ");
return [NSString stringWithCPPString: text];
}
return @"";
}
return nil;
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if (mData && aTableColumn.identifier != nil)
{
if (anObject == nil)
{
if (!(*mData)->is_field_null(rowIndex, aTableColumn.identifier.intValue))
(*mData)->set_field_null(rowIndex, aTableColumn.identifier.intValue);
}
else
{
std::string new_text= [anObject UTF8String];
std::string old_text;
(*mData)->get_field(rowIndex, aTableColumn.identifier.intValue, old_text);
if (old_text != new_text)
{
size_t oldRowCount= (*mData)->count();
(*mData)->set_field(rowIndex, aTableColumn.identifier.intValue,
new_text);
if ((*mData)->count() > oldRowCount)
[aTableView noteNumberOfRowsChanged];
}
}
}
else if (mData && aTableColumn == nil)
{
// delete row
(*mData)->delete_node(rowIndex);
[aTableView noteNumberOfRowsChanged];
}
}
- (void) tableView: (NSTableView*) aTableView
willDisplayCell: (id) aCell
forTableColumn: (NSTableColumn*) aTableColumn
row: (NSInteger) rowIndex;
{
if (aTableColumn.identifier && ![aTableColumn.identifier isEqualToString: @""])
{
int columnIndex = aTableColumn.identifier.intValue;
if (columnIndex >= 0)
{
[aCell setIsNull: (*mData)->is_field_null(rowIndex, columnIndex)];
[aCell setIsBlob: (*mData)->get_column_type(columnIndex) == bec::GridModel::BlobType];
}
else
{
[aCell setIsNull: NO];
[aCell setIsBlob: NO];
}
}
else
{
[aCell setSelected: ((MGridView*)aTableView).selectedRowIndex == rowIndex];
}
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
if (mData)
return (*mData)->count();
return 0;
}
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if (mData && aTableColumn.identifier != nil)
return !(*mData)->is_readonly() &&
(*mData)->get_column_type(aTableColumn.identifier.intValue) != bec::GridModel::BlobType;
return NO;
}
- (void)close
{
(*mData)->close();
}
- (void) tableView: (NSTableView *) tableView
didClickTableColumn: (NSTableColumn *) tableColumn
{
if (tableColumn.identifier)
{
int column_index= tableColumn.identifier.intValue;
::bec::GridModel::SortColumns sort_columns= (*mData)->sort_columns();
int sort_order= 1; // ascending (1) on first click, descending (-1) on second, then toggling
for (::bec::GridModel::SortColumns::const_iterator i= sort_columns.begin(), end= sort_columns.end(); i != end; ++i)
{
if ((int)i->first == column_index)
{
sort_order= (1 == i->second) ? -1 : 0;
break;
}
}
(*mData)->sort_by(column_index, sort_order, true);
[self setHeaderIndicator: sort_order forColumn: column_index];
}
else
{
// reset sorting if clicked the dummy column - change this to Select All, to match behaviour in Windows
//[mTableView setIndicatorImage:nil inTableColumn:tableColumn];
//(*mData)->sort_by(0, 0, false);
}
}
@end